mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-03-09 03:24:06 +00:00
use new QueryStatistics in API server
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# This file is part of Nominatim. (https://nominatim.org)
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
#
|
#
|
||||||
# Copyright (C) 2024 by the Nominatim developer community.
|
# Copyright (C) 2025 by the Nominatim developer community.
|
||||||
# For a full list of authors see the git log.
|
# For a full list of authors see the git log.
|
||||||
"""
|
"""
|
||||||
Base abstraction for implementing based on different ASGI frameworks.
|
Base abstraction for implementing based on different ASGI frameworks.
|
||||||
@@ -13,6 +13,7 @@ import math
|
|||||||
|
|
||||||
from ..config import Configuration
|
from ..config import Configuration
|
||||||
from ..core import NominatimAPIAsync
|
from ..core import NominatimAPIAsync
|
||||||
|
from ..types import QueryStatistics
|
||||||
from ..result_formatting import FormatDispatcher
|
from ..result_formatting import FormatDispatcher
|
||||||
from .content_types import CONTENT_TEXT
|
from .content_types import CONTENT_TEXT
|
||||||
|
|
||||||
@@ -68,6 +69,12 @@ class ASGIAdaptor(abc.ABC):
|
|||||||
""" Return the formatting object to use.
|
""" Return the formatting object to use.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def query_stats(self) -> Optional[QueryStatistics]:
|
||||||
|
""" Return the object for saving query statistics or None if
|
||||||
|
no statistics are required.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
def get_int(self, name: str, default: Optional[int] = None) -> int:
|
||||||
""" Return an input parameter as an int. Raises an exception if
|
""" Return an input parameter as an int. Raises an exception if
|
||||||
the parameter is given but not in an integer format.
|
the parameter is given but not in an integer format.
|
||||||
|
|||||||
@@ -2,20 +2,21 @@
|
|||||||
#
|
#
|
||||||
# This file is part of Nominatim. (https://nominatim.org)
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
#
|
#
|
||||||
# Copyright (C) 2024 by the Nominatim developer community.
|
# Copyright (C) 2025 by the Nominatim developer community.
|
||||||
# For a full list of authors see the git log.
|
# For a full list of authors see the git log.
|
||||||
"""
|
"""
|
||||||
Server implementation using the falcon webserver framework.
|
Server implementation using the falcon webserver framework.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Mapping, Any, List
|
from typing import Optional, Mapping, Any, List, cast
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import datetime as dt
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
from falcon.asgi import App, Request, Response
|
from falcon.asgi import App, Request, Response
|
||||||
|
|
||||||
from ...config import Configuration
|
from ...config import Configuration
|
||||||
from ...core import NominatimAPIAsync
|
from ...core import NominatimAPIAsync
|
||||||
|
from ...types import QueryStatistics
|
||||||
from ... import v1 as api_impl
|
from ... import v1 as api_impl
|
||||||
from ...result_formatting import FormatDispatcher, load_format_dispatcher
|
from ...result_formatting import FormatDispatcher, load_format_dispatcher
|
||||||
from ... import logging as loglib
|
from ... import logging as loglib
|
||||||
@@ -95,6 +96,9 @@ class ParamWrapper(ASGIAdaptor):
|
|||||||
def formatting(self) -> FormatDispatcher:
|
def formatting(self) -> FormatDispatcher:
|
||||||
return self._formatter
|
return self._formatter
|
||||||
|
|
||||||
|
def query_stats(self) -> Optional[QueryStatistics]:
|
||||||
|
return cast(Optional[QueryStatistics], getattr(self.request.context, 'query_stats', None))
|
||||||
|
|
||||||
|
|
||||||
class EndpointWrapper:
|
class EndpointWrapper:
|
||||||
""" Converter for server glue endpoint functions to Falcon request handlers.
|
""" Converter for server glue endpoint functions to Falcon request handlers.
|
||||||
@@ -124,7 +128,7 @@ class FileLoggingMiddleware:
|
|||||||
async def process_request(self, req: Request, _: Response) -> None:
|
async def process_request(self, req: Request, _: Response) -> None:
|
||||||
""" Callback before the request starts timing.
|
""" Callback before the request starts timing.
|
||||||
"""
|
"""
|
||||||
req.context.start = dt.datetime.now(tz=dt.timezone.utc)
|
req.context.query_stats = QueryStatistics()
|
||||||
|
|
||||||
async def process_response(self, req: Request, resp: Response,
|
async def process_response(self, req: Request, resp: Response,
|
||||||
resource: Optional[EndpointWrapper],
|
resource: Optional[EndpointWrapper],
|
||||||
@@ -132,19 +136,23 @@ class FileLoggingMiddleware:
|
|||||||
""" Callback after requests writes to the logfile. It only
|
""" Callback after requests writes to the logfile. It only
|
||||||
writes logs for successful requests for search, reverse and lookup.
|
writes logs for successful requests for search, reverse and lookup.
|
||||||
"""
|
"""
|
||||||
if not req_succeeded or resource is None or resp.status != 200\
|
qs = req.context.query_stats
|
||||||
|
|
||||||
|
if not req_succeeded or 'start' not in qs\
|
||||||
|
or resource is None or resp.status != 200\
|
||||||
or resource.name not in ('reverse', 'search', 'lookup', 'details'):
|
or resource.name not in ('reverse', 'search', 'lookup', 'details'):
|
||||||
return
|
return
|
||||||
|
|
||||||
finish = dt.datetime.now(tz=dt.timezone.utc)
|
qs['endpoint'] = resource.name
|
||||||
duration = (finish - req.context.start).total_seconds()
|
qs['query_string'] = req.scope['query_string'].decode('utf8')
|
||||||
params = req.scope['query_string'].decode('utf8')
|
qs['results_total'] = getattr(resp.context, 'num_results', 0)
|
||||||
start = req.context.start.replace(tzinfo=None)\
|
for param in ('start', 'end', 'start_query'):
|
||||||
.isoformat(sep=' ', timespec='milliseconds')
|
if isinstance(qs.get(param), dt.datetime):
|
||||||
|
qs[param] = qs[param].replace(tzinfo=None)\
|
||||||
|
.isoformat(sep=' ', timespec='milliseconds')
|
||||||
|
|
||||||
self.fd.write(f"[{start}] "
|
self.fd.write(("[{start}] {total_time:.4f} {results_total} "
|
||||||
f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
|
'{endpoint} "{query_string}"\n').format_map(qs))
|
||||||
f'{resource.name} "{params}"\n')
|
|
||||||
|
|
||||||
|
|
||||||
class APIMiddleware:
|
class APIMiddleware:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# This file is part of Nominatim. (https://nominatim.org)
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
#
|
#
|
||||||
# Copyright (C) 2024 by the Nominatim developer community.
|
# Copyright (C) 2025 by the Nominatim developer community.
|
||||||
# For a full list of authors see the git log.
|
# For a full list of authors see the git log.
|
||||||
"""
|
"""
|
||||||
Server implementation using the starlette webserver framework.
|
Server implementation using the starlette webserver framework.
|
||||||
@@ -10,9 +10,9 @@ Server implementation using the starlette webserver framework.
|
|||||||
from typing import Any, Optional, Mapping, Callable, cast, Coroutine, Dict, \
|
from typing import Any, Optional, Mapping, Callable, cast, Coroutine, Dict, \
|
||||||
Awaitable, AsyncIterator
|
Awaitable, AsyncIterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import datetime as dt
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
@@ -25,6 +25,7 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
|
|
||||||
from ...config import Configuration
|
from ...config import Configuration
|
||||||
from ...core import NominatimAPIAsync
|
from ...core import NominatimAPIAsync
|
||||||
|
from ...types import QueryStatistics
|
||||||
from ... import v1 as api_impl
|
from ... import v1 as api_impl
|
||||||
from ...result_formatting import FormatDispatcher, load_format_dispatcher
|
from ...result_formatting import FormatDispatcher, load_format_dispatcher
|
||||||
from ..asgi_adaptor import ASGIAdaptor, EndpointFunc
|
from ..asgi_adaptor import ASGIAdaptor, EndpointFunc
|
||||||
@@ -70,6 +71,9 @@ class ParamWrapper(ASGIAdaptor):
|
|||||||
def formatting(self) -> FormatDispatcher:
|
def formatting(self) -> FormatDispatcher:
|
||||||
return cast(FormatDispatcher, self.request.app.state.formatter)
|
return cast(FormatDispatcher, self.request.app.state.formatter)
|
||||||
|
|
||||||
|
def query_stats(self) -> Optional[QueryStatistics]:
|
||||||
|
return cast(Optional[QueryStatistics], getattr(self.request.state, 'query_stats', None))
|
||||||
|
|
||||||
|
|
||||||
def _wrap_endpoint(func: EndpointFunc)\
|
def _wrap_endpoint(func: EndpointFunc)\
|
||||||
-> Callable[[Request], Coroutine[Any, Any, Response]]:
|
-> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||||
@@ -89,27 +93,29 @@ class FileLoggingMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
async def dispatch(self, request: Request,
|
async def dispatch(self, request: Request,
|
||||||
call_next: RequestResponseEndpoint) -> Response:
|
call_next: RequestResponseEndpoint) -> Response:
|
||||||
start = dt.datetime.now(tz=dt.timezone.utc)
|
qs = QueryStatistics()
|
||||||
|
request.state.query_stats = qs
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200 or 'start' not in qs:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
finish = dt.datetime.now(tz=dt.timezone.utc)
|
|
||||||
|
|
||||||
for endpoint in ('reverse', 'search', 'lookup', 'details'):
|
for endpoint in ('reverse', 'search', 'lookup', 'details'):
|
||||||
if request.url.path.startswith('/' + endpoint):
|
if request.url.path.startswith('/' + endpoint):
|
||||||
qtype = endpoint
|
qs['endpoint'] = endpoint
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
duration = (finish - start).total_seconds()
|
qs['query_string'] = request.scope['query_string'].decode('utf8')
|
||||||
params = request.scope['query_string'].decode('utf8')
|
qs['results_total'] = getattr(request.state, 'num_results', 0)
|
||||||
|
for param in ('start', 'end', 'start_query'):
|
||||||
|
if isinstance(qs.get(param), dt.datetime):
|
||||||
|
qs[param] = qs[param].replace(tzinfo=None)\
|
||||||
|
.isoformat(sep=' ', timespec='milliseconds')
|
||||||
|
|
||||||
self.fd.write(f"[{start.replace(tzinfo=None).isoformat(sep=' ', timespec='milliseconds')}] "
|
self.fd.write(("[{start}] {total_time:.4f} {results_total} "
|
||||||
f"{duration:.4f} {getattr(request.state, 'num_results', 0)} "
|
'{endpoint} "{query_string}"\n').format_map(qs))
|
||||||
f'{qtype} "{params}"\n')
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -340,18 +340,17 @@ class QueryStatistics(dict[str, Any]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __enter__(self) -> 'QueryStatistics':
|
def __enter__(self) -> 'QueryStatistics':
|
||||||
self.log_time('start_function')
|
self.log_time('start')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *_: Any) -> None:
|
def __exit__(self, *_: Any) -> None:
|
||||||
self.log_time('end_function')
|
self.log_time('end')
|
||||||
self['total_time'] = (self['end_function'] - self['start_function']) \
|
self['total_time'] = (self['end'] - self['start']).total_seconds()
|
||||||
/ dt.timedelta(microseconds=1)
|
|
||||||
if 'start_query' in self:
|
if 'start_query' in self:
|
||||||
self['wait_time'] = (self['start_query'] - self['start_function']) \
|
self['wait_time'] = (self['start_query'] - self['start']).total_seconds()
|
||||||
/ dt.timedelta(microseconds=1)
|
|
||||||
else:
|
else:
|
||||||
self['wait_time'] = 0
|
self['wait_time'] = self['total_time']
|
||||||
|
self['start_query'] = self['end']
|
||||||
self['query_time'] = self['total_time'] - self['wait_time']
|
self['query_time'] = self['total_time'] - self['wait_time']
|
||||||
|
|
||||||
def __missing__(self, key: str) -> str:
|
def __missing__(self, key: str) -> str:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# This file is part of Nominatim. (https://nominatim.org)
|
# This file is part of Nominatim. (https://nominatim.org)
|
||||||
#
|
#
|
||||||
# Copyright (C) 2024 by the Nominatim developer community.
|
# Copyright (C) 2025 by the Nominatim developer community.
|
||||||
# For a full list of authors see the git log.
|
# For a full list of authors see the git log.
|
||||||
"""
|
"""
|
||||||
Generic part of the server implementation of the v1 API.
|
Generic part of the server implementation of the v1 API.
|
||||||
@@ -165,6 +165,7 @@ async def details_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
|||||||
geometry_output=(GeometryFormat.GEOJSON
|
geometry_output=(GeometryFormat.GEOJSON
|
||||||
if params.get_bool('polygon_geojson', False)
|
if params.get_bool('polygon_geojson', False)
|
||||||
else GeometryFormat.NONE),
|
else GeometryFormat.NONE),
|
||||||
|
query_stats=params.query_stats()
|
||||||
)
|
)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
@@ -197,6 +198,7 @@ async def reverse_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
|||||||
details = parse_geometry_details(params, fmt)
|
details = parse_geometry_details(params, fmt)
|
||||||
details['max_rank'] = helpers.zoom_to_rank(params.get_int('zoom', 18))
|
details['max_rank'] = helpers.zoom_to_rank(params.get_int('zoom', 18))
|
||||||
details['layers'] = get_layers(params)
|
details['layers'] = get_layers(params)
|
||||||
|
details['query_stats'] = params.query_stats()
|
||||||
|
|
||||||
result = await api.reverse(coord, **details)
|
result = await api.reverse(coord, **details)
|
||||||
|
|
||||||
@@ -234,6 +236,7 @@ async def lookup_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
|||||||
fmt = parse_format(params, SearchResults, 'xml')
|
fmt = parse_format(params, SearchResults, 'xml')
|
||||||
debug = setup_debugging(params)
|
debug = setup_debugging(params)
|
||||||
details = parse_geometry_details(params, fmt)
|
details = parse_geometry_details(params, fmt)
|
||||||
|
details['query_stats'] = params.query_stats()
|
||||||
|
|
||||||
places = []
|
places = []
|
||||||
for oid in (params.get('osm_ids') or '').split(','):
|
for oid in (params.get('osm_ids') or '').split(','):
|
||||||
@@ -302,6 +305,7 @@ async def search_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
|||||||
debug = setup_debugging(params)
|
debug = setup_debugging(params)
|
||||||
details = parse_geometry_details(params, fmt)
|
details = parse_geometry_details(params, fmt)
|
||||||
|
|
||||||
|
details['query_stats'] = params.query_stats()
|
||||||
details['countries'] = params.get('countrycodes', None)
|
details['countries'] = params.get('countrycodes', None)
|
||||||
details['entrances'] = params.get_bool('entrances', False)
|
details['entrances'] = params.get_bool('entrances', False)
|
||||||
details['excluded'] = params.get('exclude_place_ids', None)
|
details['excluded'] = params.get('exclude_place_ids', None)
|
||||||
|
|||||||
@@ -54,3 +54,6 @@ class FakeAdaptor(glue.ASGIAdaptor):
|
|||||||
|
|
||||||
def formatting(self):
|
def formatting(self):
|
||||||
return formatting
|
return formatting
|
||||||
|
|
||||||
|
def query_stats(self):
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user