]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/server/falcon/server.py
reintroduce file logging for Python frontend
[nominatim.git] / nominatim / server / falcon / server.py
1 # SPDX-License-Identifier: GPL-2.0-only
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 Server implementation using the falcon webserver framework.
9 """
10 from typing import Optional, Mapping, cast, Any
11 from pathlib import Path
12 import datetime as dt
13
14 from falcon.asgi import App, Request, Response
15
16 from nominatim.api import NominatimAPIAsync
17 import nominatim.api.v1 as api_impl
18 from nominatim.config import Configuration
19
20 class HTTPNominatimError(Exception):
21     """ A special exception class for errors raised during processing.
22     """
23     def __init__(self, msg: str, status: int, content_type: str) -> None:
24         self.msg = msg
25         self.status = status
26         self.content_type = content_type
27
28
29 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
30                                   exception: HTTPNominatimError,
31                                   _: Any) -> None:
32     """ Special error handler that passes message and content type as
33         per exception info.
34     """
35     resp.status = exception.status
36     resp.text = exception.msg
37     resp.content_type = exception.content_type
38
39
40 class ParamWrapper(api_impl.ASGIAdaptor):
41     """ Adaptor class for server glue to Falcon framework.
42     """
43
44     def __init__(self, req: Request, resp: Response,
45                  config: Configuration) -> None:
46         self.request = req
47         self.response = resp
48         self._config = config
49
50
51     def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
52         return cast(Optional[str], self.request.get_param(name, default=default))
53
54
55     def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
56         return cast(Optional[str], self.request.get_header(name, default=default))
57
58
59     def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
60         return HTTPNominatimError(msg, status, self.content_type)
61
62
63     def create_response(self, status: int, output: str, num_results: int) -> None:
64         self.response.context.num_results = num_results
65         self.response.status = status
66         self.response.text = output
67         self.response.content_type = self.content_type
68
69
70     def config(self) -> Configuration:
71         return self._config
72
73
74 class EndpointWrapper:
75     """ Converter for server glue endpoint functions to Falcon request handlers.
76     """
77
78     def __init__(self, name: str, func: api_impl.EndpointFunc, api: NominatimAPIAsync) -> None:
79         self.name = name
80         self.func = func
81         self.api = api
82
83
84     async def on_get(self, req: Request, resp: Response) -> None:
85         """ Implementation of the endpoint.
86         """
87         await self.func(self.api, ParamWrapper(req, resp, self.api.config))
88
89
90 class FileLoggingMiddleware:
91     """ Middleware to log selected requests into a file.
92     """
93
94     def __init__(self, file_name: str):
95         self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
96
97
98     async def process_request(self, req: Request, _: Response) -> None:
99         """ Callback before the request starts timing.
100         """
101         req.context.start = dt.datetime.now(tz=dt.timezone.utc)
102
103
104     async def process_response(self, req: Request, resp: Response,
105                                resource: Optional[EndpointWrapper],
106                                req_succeeded: bool) -> None:
107         """ Callback after requests writes to the logfile. It only
108             writes logs for sucessful requests for search, reverse and lookup.
109         """
110         if not req_succeeded or resource is None or resp.status != 200\
111             or resource.name not in ('reverse', 'search', 'lookup'):
112             return
113
114         finish = dt.datetime.now(tz=dt.timezone.utc)
115         duration = (finish - req.context.start).total_seconds()
116         params = req.scope['query_string'].decode('utf8')
117         start = req.context.start.replace(tzinfo=None)\
118                                  .isoformat(sep=' ', timespec='milliseconds')
119
120         self.fd.write(f"[{start}] "
121                       f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
122                       f'{resource.name} "{params}"\n')
123
124
125 def get_application(project_dir: Path,
126                     environ: Optional[Mapping[str, str]] = None) -> App:
127     """ Create a Nominatim Falcon ASGI application.
128     """
129     api = NominatimAPIAsync(project_dir, environ)
130
131     middleware: Optional[object] = None
132     log_file = api.config.LOG_FILE
133     if log_file:
134         middleware = FileLoggingMiddleware(log_file)
135
136     app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
137               middleware=middleware)
138     app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
139
140     legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
141     for name, func in api_impl.ROUTES:
142         endpoint = EndpointWrapper(name, func, api)
143         app.add_route(f"/{name}", endpoint)
144         if legacy_urls:
145             app.add_route(f"/{name}.php", endpoint)
146
147     return app
148
149
150 def run_wsgi() -> App:
151     """ Entry point for uvicorn.
152
153         Make sure uvicorn is run from the project directory.
154     """
155     return get_application(Path('.'))