Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/HH-230913' into EXP-99073
Browse files Browse the repository at this point in the history
  • Loading branch information
HH ReleaseBot committed Sep 19, 2024
2 parents 4778535 + e88ccc2 commit 152265a
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 66 deletions.
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

0 comments on commit 152265a

Please sign in to comment.