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:
72 return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
75 class HTMLLogger(BaseLogger):
76 """ Logger that formats messages in HTML.
78 def __init__(self) -> None:
79 self.buffer = io.StringIO()
82 def get_buffer(self) -> str:
83 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
86 def function(self, func: str, **kwargs: Any) -> None:
87 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
88 for name, value in kwargs.items():
89 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
90 self._write('</dl></p>')
93 def section(self, heading: str) -> None:
94 self._write(f"<h2>{heading}</h2>")
97 def comment(self, text: str) -> None:
98 self._write(f"<p>{text}</p>")
101 def var_dump(self, heading: str, var: Any) -> None:
102 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
105 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
106 sqlstr = self.format_sql(conn, statement)
108 sqlstr = highlight(sqlstr, PostgresLexer(),
109 HtmlFormatter(nowrap=True, lineseparator='<br />'))
110 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
112 self._write(f'<code class="lang-sql">{sqlstr}</code>')
115 def _python_var(self, var: Any) -> str:
117 fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
118 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
120 return f'<code class="lang-python">{str(var)}</code>'
123 def _write(self, text: str) -> None:
124 """ Add the raw text to the debug output.
126 self.buffer.write(text)
129 class TextLogger(BaseLogger):
130 """ Logger creating output suitable for the console.
132 def __init__(self) -> None:
133 self.buffer = io.StringIO()
136 def get_buffer(self) -> str:
137 return self.buffer.getvalue()
140 def function(self, func: str, **kwargs: Any) -> None:
141 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
142 for name, value in kwargs.items():
143 self._write(f' {name}: {self._python_var(value)}\n')
147 def section(self, heading: str) -> None:
148 self._write(f"\n# {heading}\n\n")
151 def comment(self, text: str) -> None:
152 self._write(f"{text}\n")
155 def var_dump(self, heading: str, var: Any) -> None:
156 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
159 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
160 sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement), width=78))
161 self._write(f"| {sqlstr}\n\n")
164 def _python_var(self, var: Any) -> str:
168 def _write(self, text: str) -> None:
169 self.buffer.write(text)
172 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
175 def set_log_output(fmt: str) -> None:
176 """ Enable collecting debug information.
179 logger.set(HTMLLogger())
181 logger.set(TextLogger())
183 logger.set(BaseLogger())
186 def log() -> BaseLogger:
187 """ Return the logger for the current context.
192 def get_and_disable() -> str:
193 """ Return the current content of the debug buffer and disable logging.
195 buf = logger.get().get_buffer()
196 logger.set(BaseLogger())
200 HTML_HEADER: str = """<!DOCTYPE html>
203 <title>Nominatim - Debug</title>
206 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
208 h2 { font-size: x-large }
212 font-family: monospace
221 dt::after { content: ": "; }
234 border: solid lightgrey 0.1pt;
236 background-color: #f7f7f7
241 border: solid lightgrey 0.1pt
248 HTML_FOOTER: str = "</body></html>"