Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HH-230913 small fixes #741

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions frontik/app_integrations/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,7 @@ def generate_trace_id(self) -> int:
request_id = request_context.get_request_id()
try:
if request_id is None:
msg = 'bad request_id'
raise Exception(msg)
raise Exception('bad request_id')

if len(request_id) < 32:
log.debug('request_id = %s is less than 32 characters. Generating random trace_id', request_id)
Expand Down
4 changes: 3 additions & 1 deletion frontik/frontik_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(
status_code: int,
headers: dict[str, str] | None | HTTPHeaders = None,
body: bytes = b'',
reason: str | None = None,
):
self.headers = HTTPHeaders(get_default_headers()) # type: ignore

Expand All @@ -26,11 +27,12 @@ def __init__(

self.status_code = status_code
self.body = body
self._reason = reason
self.data_written = False

@property
def reason(self) -> str:
return httputil.responses.get(self.status_code, 'Unknown')
return self._reason or httputil.responses.get(self.status_code, 'Unknown')


def get_default_headers() -> Mapping[str, str | None]:
Expand Down
17 changes: 7 additions & 10 deletions frontik/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from frontik import media_types, request_context
from frontik.auth import DEBUG_AUTH_HEADER_NAME
from frontik.debug import DEBUG_HEADER_NAME, DebugMode
from frontik.frontik_response import FrontikResponse
from frontik.futures import AbortAsyncGroup, AsyncGroup
from frontik.http_status import ALLOWED_STATUSES, NON_CRITICAL_BAD_GATEWAY
from frontik.json_builder import FrontikJsonDecodeError, json_decode
Expand All @@ -39,13 +40,12 @@
from frontik.timeout_tracking import get_timeout_checker
from frontik.util import gather_dict, make_url
from frontik.validator import BaseValidationModel, Validators
from frontik.version import version as frontik_version

if TYPE_CHECKING:
from collections.abc import Callable, Coroutine

from http_client import HttpClient
from tornado.httputil import HTTPHeaders, HTTPServerRequest
from tornado.httputil import HTTPServerRequest

from frontik.app import FrontikApplication
from frontik.app_integrations.statsd import StatsDClient, StatsDClientStub
Expand Down Expand Up @@ -111,7 +111,7 @@ def __init__(
path_params: dict[str, str],
) -> None:
self.name = self.__class__.__name__
self.request_id: str = request_context.get_request_id() # type: ignore
self.request_id: str = request.request_id # type: ignore
self.config = application.config
self.log = handler_logger
self.text: Any = None
Expand Down Expand Up @@ -158,7 +158,7 @@ def __init__(
request._start_time,
)

self.handler_result_future: Future[tuple[int, str, HTTPHeaders, bytes]] = Future()
self.handler_result_future: Future[FrontikResponse] = Future()

def __repr__(self):
return f'{self.__module__}.{self.__class__.__name__}'
Expand All @@ -183,10 +183,7 @@ def prepare(self) -> None:
super().prepare()

def set_default_headers(self):
self._headers = httputil.HTTPHeaders({
'Server': f'Frontik/{frontik_version}',
'X-Request-Id': self.request_id,
})
self._headers = httputil.HTTPHeaders()

@property
def path(self) -> str:
Expand Down Expand Up @@ -370,7 +367,7 @@ def add_future(cls, future: Future, callback: Callable) -> None:

# Requests handling

async def execute(self) -> tuple[int, str, HTTPHeaders, bytes]:
async def execute(self) -> FrontikResponse:
self._transforms = []
try:
if self.request.method not in self.SUPPORTED_METHODS:
Expand Down Expand Up @@ -701,7 +698,7 @@ def _flush(self) -> None:
for cookie in self._new_cookie.values():
self.add_header('Set-Cookie', cookie.OutputString(None))

self.handler_result_future.set_result((self._status_code, self._reason, self._headers, chunk))
self.handler_result_future.set_result(FrontikResponse(self._status_code, self._headers, chunk, self._reason))

# postprocessors

Expand Down
20 changes: 11 additions & 9 deletions frontik/handler_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,17 @@ async def execute_tornado_page(
route, page_cls, path_params = scope['route'], scope['page_cls'], scope['path_params']
request_context.set_handler_name(route)
handler: PageHandler = page_cls(frontik_app, tornado_request, route, debug_mode, path_params)
status_code, _, headers, body = await handler.execute()
return FrontikResponse(status_code=status_code, headers=headers, body=body)
return await handler.execute()


def _on_connection_close(tornado_request, process_request_task, integrations):
response = FrontikResponse(CLIENT_CLOSED_REQUEST)
for integration in integrations.values():
integration.set_response(response)

log_request(tornado_request, CLIENT_CLOSED_REQUEST)
setattr(tornado_request, 'canceled', False)
process_request_task.cancel() # serve_tornado_request will be interrupted with CanceledError
request_id = integrations.get('request_id', IntegrationDto()).get_value()
with request_context.request_context(request_id):
log.info('client has canceled request')
response = FrontikResponse(CLIENT_CLOSED_REQUEST)
for integration in integrations.values():
integration.set_response(response)

log_request(tornado_request, CLIENT_CLOSED_REQUEST)
setattr(tornado_request, 'canceled', False)
process_request_task.cancel() # serve_tornado_request will be interrupted with CanceledError
17 changes: 5 additions & 12 deletions frontik/loggers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@

class Mdc:
def __init__(self) -> None:
self.pid: int
self.role: Union[str, None] = None

def init(self, role: Union[str, None] = None) -> None:
self.pid = os.getpid()
self.role = role


Expand All @@ -40,8 +38,7 @@ def init(self, role: Union[str, None] = None) -> None:
class ContextFilter(Filter):
def filter(self, record):
handler_name = request_context.get_handler_name()
request_id = request_context.get_request_id()
record.name = '.'.join(filter(None, [record.name, handler_name, request_id]))
record.name = '.'.join(filter(None, [record.name, handler_name]))
return True


Expand Down Expand Up @@ -94,7 +91,7 @@ def format(self, record):

@staticmethod
def get_mdc() -> dict:
mdc: dict = {'thread': MDC.pid}
mdc: dict = {'thread': os.getpid()}

if MDC.role is not None:
mdc['role'] = MDC.role
Expand Down Expand Up @@ -126,15 +123,11 @@ def format_stack_trace(self, record: logging.LogRecord) -> str:
return stack_trace


_JSON_FORMATTER = JSONFormatter()
JSON_FORMATTER = JSONFormatter()


class StderrFormatter(LogFormatter):
def format(self, record):
handler_name = request_context.get_handler_name()
request_id = request_context.get_request_id()
record.name = '.'.join(filter(None, [record.name, handler_name, request_id]))

if not record.msg:
record.msg = ', '.join(f'{k}={v}' for k, v in getattr(record, CUSTOM_JSON_EXTRA, {}).items())

Expand Down Expand Up @@ -197,7 +190,7 @@ def _configure_file(
if formatter is not None:
file_handler.setFormatter(formatter)
elif use_json_formatter:
file_handler.setFormatter(_JSON_FORMATTER)
file_handler.setFormatter(JSON_FORMATTER)
else:
file_handler.setFormatter(get_text_formatter())
file_handler.addFilter(_CONTEXT_FILTER)
Expand Down Expand Up @@ -232,7 +225,7 @@ def _configure_syslog(
if formatter is not None:
syslog_handler.setFormatter(formatter)
elif use_json_formatter:
syslog_handler.setFormatter(_JSON_FORMATTER)
syslog_handler.setFormatter(JSON_FORMATTER)
else:
syslog_handler.setFormatter(get_text_formatter())
syslog_handler.addFilter(_CONTEXT_FILTER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from fastapi import HTTPException
from http_client import HttpClientFactory

from frontik import request_context
from frontik.loggers.logleveloverride.log_level_override_extension import LogLevelOverride, LogLevelOverrideExtension
from frontik.loggers.logleveloverride.logging_configurator_client import LOG_LEVEL_MAPPING
from frontik.util import generate_uniq_timestamp_request_id

logger = logging.getLogger('http_log_level_override_extension')

Expand All @@ -33,7 +33,7 @@ def __init__(self, host: str, uri: str, http_client_factory: HttpClientFactory)
self.http_client_factory = http_client_factory

async def load_log_level_overrides(self) -> list[LogLevelOverride]:
headers = {'X-Request-Id': request_context.get_request_id()}
headers = {'X-Request-Id': generate_uniq_timestamp_request_id()}
result = await self.http_client_factory.get_http_client().get_url(self.host, self.uri, headers=headers)
if result.failed:
logger.error('some problem with fetching log level overrides: %s', result.failed)
Expand Down
12 changes: 12 additions & 0 deletions frontik/request_context.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from __future__ import annotations

import contextvars
from contextlib import contextmanager
from typing import TYPE_CHECKING, Optional

from fastapi.routing import APIRoute

if TYPE_CHECKING:
from collections.abc import Iterator

from frontik.debug import DebugBufferedHandler


Expand All @@ -21,6 +24,15 @@ def __init__(self, request_id: Optional[str]) -> None:
_context = contextvars.ContextVar('context', default=_Context(None))


@contextmanager
def request_context(request_id: Optional[str]) -> Iterator:
token = _context.set(_Context(request_id))
try:
yield
finally:
_context.reset(token)


def get_request_id() -> Optional[str]:
return _context.get().request_id

Expand Down
7 changes: 2 additions & 5 deletions frontik/request_integrations/request_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,5 @@ def request_id_ctx(_, tornado_request):
check_request_id(request_id)
tornado_request.request_id = request_id

token = request_context._context.set(request_context._Context(request_id))
try:
yield IntegrationDto()
finally:
request_context._context.reset(token)
with request_context.request_context(request_id):
yield IntegrationDto(request_id)
7 changes: 7 additions & 0 deletions frontik/request_integrations/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from opentelemetry.util.http import normalise_response_header_name
from opentelemetry.trace.status import Status, StatusCode
from tornado import httputil
from frontik import request_context

_traced_request_attrs = get_traced_request_attrs('TORNADO')
_excluded_urls = ['/status']
Expand Down Expand Up @@ -86,6 +87,12 @@ def _finish_span(span, dto: IntegrationDto):

if span.is_recording():
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
if (handler_name := request_context.get_handler_name()) is not None:
method_path, method_name = handler_name.rsplit('.', 1)
span.update_name(f'{method_path}.{method_name}')
span.set_attribute(SpanAttributes.CODE_FUNCTION, method_name)
span.set_attribute(SpanAttributes.CODE_NAMESPACE, method_path)

otel_status_code = http_status_to_status_code(status_code, server_span=True)
otel_status_description = None
if otel_status_code is StatusCode.ERROR:
Expand Down
4 changes: 4 additions & 0 deletions frontik/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def event_loop(self):
yield loop
loop.close()

@pytest.fixture(scope='class')
def frontik_app(self) -> FrontikApplication:
return FrontikApplication()

@pytest.fixture(scope='class', autouse=True)
async def _run_server(self, frontik_app):
options.stderr_log = True
Expand Down
8 changes: 1 addition & 7 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import pytest
from fastapi import Response

from frontik.app import FrontikApplication
from frontik.handler import PageHandler, get_current_handler
from frontik.routing import plain_router
from frontik.testing import FrontikTestBase
Expand All @@ -19,11 +17,7 @@ async def asgi_cookies_page(response: Response) -> None:
response.set_cookie('key2', 'val2')


class TestFrontikTesting(FrontikTestBase):
@pytest.fixture(scope='class')
def frontik_app(self) -> FrontikApplication:
return FrontikApplication()

class TestCookies(FrontikTestBase):
async def test_cookies(self):
response = await self.fetch('/cookies')

Expand Down
2 changes: 1 addition & 1 deletion tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_send_to_syslog(self):
{
'priority': '10',
'message': r'\[\d+\] [\d-]+ [\d:,]+ CRITICAL '
r'custom_logger\.tests\.projects\.test_app\.pages\.log\.get_page\.\w+: fatal',
r'custom_logger\.tests\.projects\.test_app\.pages\.log\.get_page: fatal',
},
]

Expand Down
Loading
Loading