]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/logging.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / nominatim / api / logging.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Functions for specialised logging with HTML output.
9 """
10 from typing import Any, cast
11 from contextvars import ContextVar
12 import textwrap
13 import io
14
15 import sqlalchemy as sa
16 from sqlalchemy.ext.asyncio import AsyncConnection
17
18 try:
19     from pygments import highlight
20     from pygments.lexers import PythonLexer, PostgresLexer
21     from pygments.formatters import HtmlFormatter
22     CODE_HIGHLIGHT = True
23 except ModuleNotFoundError:
24     CODE_HIGHLIGHT = False
25
26
27 class BaseLogger:
28     """ Interface for logging function.
29
30         The base implementation does nothing. Overwrite the functions
31         in derived classes which implement logging functionality.
32     """
33     def get_buffer(self) -> str:
34         """ Return the current content of the log buffer.
35         """
36         return ''
37
38     def function(self, func: str, **kwargs: Any) -> None:
39         """ Start a new debug chapter for the given function and its parameters.
40         """
41
42
43     def section(self, heading: str) -> None:
44         """ Start a new section with the given title.
45         """
46
47
48     def comment(self, text: str) -> None:
49         """ Add a simple comment to the debug output.
50         """
51
52
53     def var_dump(self, heading: str, var: Any) -> None:
54         """ Print the content of the variable to the debug output prefixed by
55             the given heading.
56         """
57
58
59     def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
60         """ Print the SQL for the given statement.
61         """
62
63     def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> str:
64         """ Return the comiled version of the statement.
65         """
66         try:
67             return str(cast('sa.ClauseElement', statement)
68                          .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
69         except sa.exc.CompileError:
70             pass
71         except NotImplementedError:
72             pass
73
74         return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
75
76
77 class HTMLLogger(BaseLogger):
78     """ Logger that formats messages in HTML.
79     """
80     def __init__(self) -> None:
81         self.buffer = io.StringIO()
82
83
84     def get_buffer(self) -> str:
85         return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
86
87
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>')
93
94
95     def section(self, heading: str) -> None:
96         self._write(f"<h2>{heading}</h2>")
97
98
99     def comment(self, text: str) -> None:
100         self._write(f"<p>{text}</p>")
101
102
103     def var_dump(self, heading: str, var: Any) -> None:
104         self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
105
106
107     def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
108         sqlstr = self.format_sql(conn, statement)
109         if CODE_HIGHLIGHT:
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>')
113         else:
114             self._write(f'<code class="lang-sql">{sqlstr}</code>')
115
116
117     def _python_var(self, var: Any) -> str:
118         if CODE_HIGHLIGHT:
119             fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
120             return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
121
122         return f'<code class="lang-python">{str(var)}</code>'
123
124
125     def _write(self, text: str) -> None:
126         """ Add the raw text to the debug output.
127         """
128         self.buffer.write(text)
129
130
131 class TextLogger(BaseLogger):
132     """ Logger creating output suitable for the console.
133     """
134     def __init__(self) -> None:
135         self.buffer = io.StringIO()
136
137
138     def get_buffer(self) -> str:
139         return self.buffer.getvalue()
140
141
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')
146         self._write('\n')
147
148
149     def section(self, heading: str) -> None:
150         self._write(f"\n# {heading}\n\n")
151
152
153     def comment(self, text: str) -> None:
154         self._write(f"{text}\n")
155
156
157     def var_dump(self, heading: str, var: Any) -> None:
158         self._write(f'{heading}:\n  {self._python_var(var)}\n\n')
159
160
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")
164
165
166     def _python_var(self, var: Any) -> str:
167         return str(var)
168
169
170     def _write(self, text: str) -> None:
171         self.buffer.write(text)
172
173
174 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
175
176
177 def set_log_output(fmt: str) -> None:
178     """ Enable collecting debug information.
179     """
180     if fmt == 'html':
181         logger.set(HTMLLogger())
182     elif fmt == 'text':
183         logger.set(TextLogger())
184     else:
185         logger.set(BaseLogger())
186
187
188 def log() -> BaseLogger:
189     """ Return the logger for the current context.
190     """
191     return logger.get()
192
193
194 def get_and_disable() -> str:
195     """ Return the current content of the debug buffer and disable logging.
196     """
197     buf = logger.get().get_buffer()
198     logger.set(BaseLogger())
199     return buf
200
201
202 HTML_HEADER: str = """<!DOCTYPE html>
203 <html>
204 <head>
205   <title>Nominatim - Debug</title>
206   <style>
207 """ + \
208 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
209 """
210     h2 { font-size: x-large }
211
212     dl {
213       padding-left: 10pt;
214       font-family: monospace
215     }
216
217     dt {
218       float: left;
219       font-weight: bold;
220       margin-right: 0.5em
221     }
222
223     dt::after { content: ": "; }
224
225     dd::after {
226       clear: left;
227       display: block
228     }
229
230     .lang-sql {
231       color: #555;
232       font-size: small
233     }
234
235     h5 {
236         border: solid lightgrey 0.1pt;
237         margin-bottom: 0;
238         background-color: #f7f7f7
239     }
240
241     h5 + .highlight {
242         padding: 3pt;
243         border: solid lightgrey 0.1pt
244     }
245   </style>
246 </head>
247 <body>
248 """
249
250 HTML_FOOTER: str = "</body></html>"