]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/api/logging.py
python: implement reverse lookup function
[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
72         return str(cast('sa.ClauseElement', statement).compile(conn.sync_engine))
73
74
75 class HTMLLogger(BaseLogger):
76     """ Logger that formats messages in HTML.
77     """
78     def __init__(self) -> None:
79         self.buffer = io.StringIO()
80
81
82     def get_buffer(self) -> str:
83         return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER
84
85
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>')
91
92
93     def section(self, heading: str) -> None:
94         self._write(f"<h2>{heading}</h2>")
95
96
97     def comment(self, text: str) -> None:
98         self._write(f"<p>{text}</p>")
99
100
101     def var_dump(self, heading: str, var: Any) -> None:
102         self._write(f'<h5>{heading}</h5>{self._python_var(var)}')
103
104
105     def sql(self, conn: AsyncConnection, statement: 'sa.Executable') -> None:
106         sqlstr = self.format_sql(conn, statement)
107         if CODE_HIGHLIGHT:
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>')
111         else:
112             self._write(f'<code class="lang-sql">{sqlstr}</code>')
113
114
115     def _python_var(self, var: Any) -> str:
116         if CODE_HIGHLIGHT:
117             fmt = highlight(repr(var), PythonLexer(), HtmlFormatter(nowrap=True))
118             return f'<div class="highlight"><code class="lang-python">{fmt}</code></div>'
119
120         return f'<code class="lang-python">{str(var)}</code>'
121
122
123     def _write(self, text: str) -> None:
124         """ Add the raw text to the debug output.
125         """
126         self.buffer.write(text)
127
128
129 class TextLogger(BaseLogger):
130     """ Logger creating output suitable for the console.
131     """
132     def __init__(self) -> None:
133         self.buffer = io.StringIO()
134
135
136     def get_buffer(self) -> str:
137         return self.buffer.getvalue()
138
139
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')
144         self._write('\n')
145
146
147     def section(self, heading: str) -> None:
148         self._write(f"\n# {heading}\n\n")
149
150
151     def comment(self, text: str) -> None:
152         self._write(f"{text}\n")
153
154
155     def var_dump(self, heading: str, var: Any) -> None:
156         self._write(f'{heading}:\n  {self._python_var(var)}\n\n')
157
158
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")
162
163
164     def _python_var(self, var: Any) -> str:
165         return str(var)
166
167
168     def _write(self, text: str) -> None:
169         self.buffer.write(text)
170
171
172 logger: ContextVar[BaseLogger] = ContextVar('logger', default=BaseLogger())
173
174
175 def set_log_output(fmt: str) -> None:
176     """ Enable collecting debug information.
177     """
178     if fmt == 'html':
179         logger.set(HTMLLogger())
180     elif fmt == 'text':
181         logger.set(TextLogger())
182     else:
183         logger.set(BaseLogger())
184
185
186 def log() -> BaseLogger:
187     """ Return the logger for the current context.
188     """
189     return logger.get()
190
191
192 def get_and_disable() -> str:
193     """ Return the current content of the debug buffer and disable logging.
194     """
195     buf = logger.get().get_buffer()
196     logger.set(BaseLogger())
197     return buf
198
199
200 HTML_HEADER: str = """<!DOCTYPE html>
201 <html>
202 <head>
203   <title>Nominatim - Debug</title>
204   <style>
205 """ + \
206 (HtmlFormatter(nobackground=True).get_style_defs('.highlight') if CODE_HIGHLIGHT else '') +\
207 """
208     h2 { font-size: x-large }
209
210     dl {
211       padding-left: 10pt;
212       font-family: monospace
213     }
214
215     dt {
216       float: left;
217       font-weight: bold;
218       margin-right: 0.5em
219     }
220
221     dt::after { content: ": "; }
222
223     dd::after {
224       clear: left;
225       display: block
226     }
227
228     .lang-sql {
229       color: #555;
230       font-size: small
231     }
232
233     h5 {
234         border: solid lightgrey 0.1pt;
235         margin-bottom: 0;
236         background-color: #f7f7f7
237     }
238
239     h5 + .highlight {
240         padding: 3pt;
241         border: solid lightgrey 0.1pt
242     }
243   </style>
244 </head>
245 <body>
246 """
247
248 HTML_FOOTER: str = "</body></html>"