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, 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 sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
60 """ Print the SQL for the given statement.
63 def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> str:
64 """ Return the comiled version of the statement.
67 return str(cast('sa.ClauseElement', statement)
68 .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
69 except sa.exc.CompileError:
71 except NotImplementedError:
74 return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
77 class HTMLLogger(BaseLogger):
78 """ Logger that formats messages in HTML.
80 def __init__(self) -> None:
81 self.buffer = io.StringIO()
84 def get_buffer(self) -> str:
85 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
88 def function(self, func: str, **kwargs: Any) -> None:
89 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
90 for name, value in kwargs.items():
91 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
92 self._write('</dl></p>')
95 def section(self, heading: str) -> None:
96 self._write(f"<h2>{heading}</h2>")
99 def comment(self, text: str) -> None:
100 self._write(f"<p>{text}</p>")
103 def var_dump(self, heading: str, var: Any) -> None:
104 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
107 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
108 sqlstr = self.format_sql(conn, statement)
110 sqlstr = highlight(sqlstr, PostgresLexer(),
111 HtmlFormatter(nowrap=True, lineseparator='<br />'))
112 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
114 self._write(f'<code class="lang-sql">{sqlstr}</code>')
117 def _python_var(self, var: Any) -> str:
119 fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
120 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
122 return f'<code class="lang-python">{str(var)}</code>'
125 def _write(self, text: str) -> None:
126 """ Add the raw text to the debug output.
128 self.buffer.write(text)
131 class TextLogger(BaseLogger):
132 """ Logger creating output suitable for the console.
134 def __init__(self) -> None:
135 self.buffer = io.StringIO()
138 def get_buffer(self) -> str:
139 return self.buffer.getvalue()
142 def function(self, func: str, **kwargs: Any) -> None:
143 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
144 for name, value in kwargs.items():
145 self._write(f' {name}: {self._python_var(value)}\n')
149 def section(self, heading: str) -> None:
150 self._write(f"\n# {heading}\n\n")
153 def comment(self, text: str) -> None:
154 self._write(f"{text}\n")
157 def var_dump(self, heading: str, var: Any) -> None:
158 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
161 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
162 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78))
163 self._write(f"| {sqlstr}\n\n")
166 def _python_var(self, var: Any) -> str:
170 def _write(self, text: str) -> None:
171 self.buffer.write(text)
174 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
177 def set_log_output(fmt: str) -> None:
178 """ Enable collecting debug information.
181 logger.set(HTMLLogger())
183 logger.set(TextLogger())
185 logger.set(BaseLogger())
188 def log() -> BaseLogger:
189 """ Return the logger for the current context.
194 def get_and_disable() -> str:
195 """ Return the current content of the debug buffer and disable logging.
197 buf = logger.get().get_buffer()
198 logger.set(BaseLogger())
202 HTML_HEADER: str = """<!DOCTYPE html>
205 <title>Nominatim - Debug</title>
208 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
210 h2 { font-size: x-large }
214 font-family: monospace
223 dt::after { content: ": "; }
236 border: solid lightgrey 0.1pt;
238 background-color: #f7f7f7
243 border: solid lightgrey 0.1pt
250 HTML_FOOTER: str = "</body></html>"