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.
64 class HTMLLogger(BaseLogger):
65 """ Logger that formats messages in HTML.
67 def __init__(self) -> None:
68 self.buffer = io.StringIO()
71 def get_buffer(self) -> str:
72 return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
75 def function(self, func: str, **kwargs: Any) -> None:
76 self._write(f"<h1>Debug output for {func}()</h1>\n<p>Parameters:<dl>")
77 for name, value in kwargs.items():
78 self._write(f'<dt>{name}</dt><dd>{self._python_var(value)}</dd>')
79 self._write('</dl></p>')
82 def section(self, heading: str) -> None:
83 self._write(f"<h2>{heading}</h2>")
86 def comment(self, text: str) -> None:
87 self._write(f"<p>{text}</p>")
90 def var_dump(self, heading: str, var: Any) -> None:
91 self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
94 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
95 sqlstr = str(cast('sa.ClauseElement', statement)
96 .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
98 sqlstr = highlight(sqlstr, PostgresLexer(),
99 HtmlFormatter(nowrap=True, lineseparator='<br>'))
100 self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
102 self._write(f'<code class="lang-sql">{sqlstr}</code>')
105 def _python_var(self, var: Any) -> str:
107 fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
108 return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
110 return f'<code class="lang-python">{str(var)}</code>'
113 def _write(self, text: str) -> None:
114 """ Add the raw text to the debug output.
116 self.buffer.write(text)
119 class TextLogger(BaseLogger):
120 """ Logger creating output suitable for the console.
122 def __init__(self) -> None:
123 self.buffer = io.StringIO()
126 def get_buffer(self) -> str:
127 return self.buffer.getvalue()
130 def function(self, func: str, **kwargs: Any) -> None:
131 self._write(f"#### Debug output for {func}()\n\nParameters:\n")
132 for name, value in kwargs.items():
133 self._write(f' {name}: {self._python_var(value)}\n')
137 def section(self, heading: str) -> None:
138 self._write(f"\n# {heading}\n\n")
141 def comment(self, text: str) -> None:
142 self._write(f"{text}\n")
145 def var_dump(self, heading: str, var: Any) -> None:
146 self._write(f'{heading}:\n {self._python_var(var)}\n\n')
149 def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
150 sqlstr = str(cast('sa.ClauseElement', statement)
151 .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
152 sqlstr = '\n| '.join(textwrap.wrap(sqlstr, width=78))
153 self._write(f"| {sqlstr}\n\n")
156 def _python_var(self, var: Any) -> str:
160 def _write(self, text: str) -> None:
161 self.buffer.write(text)
164 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
167 def set_log_output(fmt: str) -> None:
168 """ Enable collecting debug information.
171 logger.set(HTMLLogger())
173 logger.set(TextLogger())
175 logger.set(BaseLogger())
178 def log() -> BaseLogger:
179 """ Return the logger for the current context.
184 def get_and_disable() -> str:
185 """ Return the current content of the debug buffer and disable logging.
187 buf = logger.get().get_buffer()
188 logger.set(BaseLogger())
192 HTML_HEADER: str = """<!DOCTYPE html>
195 <title>Nominatim - Debug</title>
198 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
200 h2 { font-size: x-large }
204 font-family: monospace
213 dt::after { content: ": "; }
226 border: solid lightgrey 0.1pt;
228 background-color: #f7f7f7
233 border: solid lightgrey 0.1pt
240 HTML_FOOTER: str = "</body></html>"