INF-3 Добавить параметр для вывода дочерних и родительских трейсов
This commit is contained in:
@@ -92,8 +92,14 @@ class TraceLogView:
|
||||
parent_id: str | None
|
||||
child_ids: tuple[str, ...] = ()
|
||||
records: tuple[TraceLogRecord, ...] = ()
|
||||
ancestors: tuple[TraceLogView, ...] = ()
|
||||
|
||||
|
||||
class TraceLogReader(Protocol):
|
||||
def read_trace(self, trace_id: str, levels: tuple[TraceLevel, ...]) -> TraceLogView | None:
|
||||
def read_trace(
|
||||
self,
|
||||
trace_id: str,
|
||||
levels: tuple[TraceLevel, ...],
|
||||
ancestor_depth: int | None = 0,
|
||||
) -> TraceLogView | None:
|
||||
"""Load trace context and filtered log records."""
|
||||
|
||||
@@ -27,6 +27,7 @@ class TraceQueryRequest:
|
||||
levels: tuple[TraceLevel, ...] = ("ERROR", "WARNING", "INFO")
|
||||
include_attrs_json: bool = False
|
||||
response_format: TraceResponseFormat = "html"
|
||||
ancestor_depth: int | None = 0
|
||||
|
||||
|
||||
TraceLookupHandler = Callable[[str, TraceQueryRequest], Awaitable[TraceLogView]]
|
||||
|
||||
@@ -22,6 +22,7 @@ class TraceRequestParser:
|
||||
levels=self._trace_levels(raw_levels),
|
||||
include_attrs_json=self._bool_param(request, "attrs_json") or False,
|
||||
response_format=response_format,
|
||||
ancestor_depth=self._ancestor_depth(request),
|
||||
)
|
||||
|
||||
def _trace_levels(self, raw_levels: str | None) -> tuple[TraceLevel, ...]:
|
||||
@@ -47,6 +48,21 @@ class TraceRequestParser:
|
||||
return False
|
||||
raise ValueError(f"invalid boolean query parameter: {name}={raw_value}")
|
||||
|
||||
def _ancestor_depth(self, request: Request) -> int | None:
|
||||
raw_value = request.query_params.get("ancestor_depth")
|
||||
if raw_value is None:
|
||||
return 0
|
||||
normalized = raw_value.strip().lower()
|
||||
if normalized == "all":
|
||||
return None
|
||||
try:
|
||||
value = int(normalized)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"invalid ancestor depth query parameter: ancestor_depth={raw_value}") from exc
|
||||
if value < 0:
|
||||
raise ValueError(f"query parameter must be >= 0: ancestor_depth={raw_value}")
|
||||
return value
|
||||
|
||||
|
||||
class TraceResponseRenderer:
|
||||
def render(self, trace_view: TraceLogView, request: TraceQueryRequest) -> Response:
|
||||
@@ -63,26 +79,14 @@ class TraceResponseRenderer:
|
||||
"parent_id": trace_view.parent_id or "",
|
||||
"child_ids": list(trace_view.child_ids),
|
||||
"messages": [record.as_dict(include_attrs_json=request.include_attrs_json) for record in trace_view.records],
|
||||
"ancestors": [self._trace_payload(view, request) for view in trace_view.ancestors],
|
||||
}
|
||||
)
|
||||
|
||||
def _render_text(self, trace_view: TraceLogView, request: TraceQueryRequest) -> PlainTextResponse:
|
||||
lines = [
|
||||
f"trace_id: {trace_view.trace_id}",
|
||||
f"parent_id: {trace_view.parent_id or ''}",
|
||||
*self._child_id_lines(trace_view.child_ids),
|
||||
"------------------------------",
|
||||
]
|
||||
previous_step: str | None = None
|
||||
for record in trace_view.records:
|
||||
current_step = str(record.step or "")
|
||||
if previous_step is None:
|
||||
lines.append(f"step: {current_step}")
|
||||
elif current_step != previous_step:
|
||||
lines.append("------------------------------")
|
||||
lines.append(f"step: {current_step}")
|
||||
previous_step = current_step
|
||||
lines.append(self._text_message(record, request.include_attrs_json))
|
||||
lines = self._text_trace_lines(trace_view, request)
|
||||
for index, ancestor in enumerate(trace_view.ancestors, start=1):
|
||||
lines.extend(["", f"ancestor[{index}]:", *self._text_trace_lines(ancestor, request)])
|
||||
return PlainTextResponse(content="\n".join(lines))
|
||||
|
||||
def _render_html(self, trace_view: TraceLogView, request: TraceQueryRequest) -> HTMLResponse:
|
||||
@@ -162,16 +166,58 @@ class TraceResponseRenderer:
|
||||
return f"{record.message}, {json.dumps(record.attrs_json, ensure_ascii=False, separators=(',', ':'))}"
|
||||
|
||||
def _trace_href(self, trace_id: str, request: TraceQueryRequest) -> str:
|
||||
params = urlencode(
|
||||
{
|
||||
"format": "html",
|
||||
"levels": ",".join(request.levels),
|
||||
"attrs_json": "true" if request.include_attrs_json else "false",
|
||||
}
|
||||
)
|
||||
return f"/traces/{trace_id}?{params}"
|
||||
params = {
|
||||
"format": "html",
|
||||
"levels": ",".join(request.levels),
|
||||
"attrs_json": "true" if request.include_attrs_json else "false",
|
||||
}
|
||||
if request.ancestor_depth is None:
|
||||
params["ancestor_depth"] = "all"
|
||||
elif request.ancestor_depth > 0:
|
||||
params["ancestor_depth"] = str(request.ancestor_depth)
|
||||
query = urlencode(params)
|
||||
return f"/traces/{trace_id}?{query}"
|
||||
|
||||
def _html_lines(self, trace_view: TraceLogView, request: TraceQueryRequest) -> str:
|
||||
lines = self._html_trace_lines(trace_view, request)
|
||||
for index, ancestor in enumerate(trace_view.ancestors, start=1):
|
||||
lines.extend(
|
||||
[
|
||||
self._html_plain_line(""),
|
||||
self._html_plain_line(f"ancestor[{index}]:"),
|
||||
*self._html_trace_lines(ancestor, request),
|
||||
]
|
||||
)
|
||||
return "".join(lines)
|
||||
|
||||
def _trace_payload(self, trace_view: TraceLogView, request: TraceQueryRequest) -> dict[str, object]:
|
||||
return {
|
||||
"trace_id": trace_view.trace_id,
|
||||
"parent_id": trace_view.parent_id or "",
|
||||
"child_ids": list(trace_view.child_ids),
|
||||
"messages": [record.as_dict(include_attrs_json=request.include_attrs_json) for record in trace_view.records],
|
||||
}
|
||||
|
||||
def _text_trace_lines(self, trace_view: TraceLogView, request: TraceQueryRequest) -> list[str]:
|
||||
lines = [
|
||||
f"trace_id: {trace_view.trace_id}",
|
||||
f"parent_id: {trace_view.parent_id or ''}",
|
||||
*self._child_id_lines(trace_view.child_ids),
|
||||
"------------------------------",
|
||||
]
|
||||
previous_step: str | None = None
|
||||
for record in trace_view.records:
|
||||
current_step = str(record.step or "")
|
||||
if previous_step is None:
|
||||
lines.append(f"step: {current_step}")
|
||||
elif current_step != previous_step:
|
||||
lines.append("------------------------------")
|
||||
lines.append(f"step: {current_step}")
|
||||
previous_step = current_step
|
||||
lines.append(self._text_message(record, request.include_attrs_json))
|
||||
return lines
|
||||
|
||||
def _html_trace_lines(self, trace_view: TraceLogView, request: TraceQueryRequest) -> list[str]:
|
||||
lines = [
|
||||
self._html_plain_line(f"trace_id: {self._trace_link(trace_view.trace_id, request)}"),
|
||||
self._html_plain_line(f"parent_id: {self._optional_trace_link(trace_view.parent_id, request)}"),
|
||||
@@ -192,7 +238,7 @@ class TraceResponseRenderer:
|
||||
lines.append(self._html_plain_line(""))
|
||||
previous_step = current_step
|
||||
lines.extend(self._html_message_lines(record, request.include_attrs_json))
|
||||
return "".join(lines)
|
||||
return lines
|
||||
|
||||
def _html_message_lines(self, record: TraceLogRecord, include_attrs_json: bool) -> list[str]:
|
||||
lines = [self._html_colored_line(self._text_message(record, include_attrs_json), record.level)]
|
||||
|
||||
@@ -141,7 +141,7 @@ class RuntimeManager:
|
||||
reader = build_trace_log_reader(self.traces.transport)
|
||||
if reader is None:
|
||||
raise RuntimeError("trace log reader is not configured")
|
||||
trace_view = reader.read_trace(trace_id, request.levels)
|
||||
trace_view = reader.read_trace(trace_id, request.levels, request.ancestor_depth)
|
||||
if trace_view is None:
|
||||
raise KeyError(trace_id)
|
||||
return trace_view
|
||||
|
||||
@@ -11,10 +11,16 @@ class MySqlTraceLogReader(TraceLogReader):
|
||||
def __init__(self, connection_factory: MySqlTraceConnectionFactory) -> None:
|
||||
self._connection_factory = connection_factory
|
||||
|
||||
def read_trace(self, trace_id: str, levels: tuple[TraceLevel, ...]) -> TraceLogView | None:
|
||||
def read_trace(
|
||||
self,
|
||||
trace_id: str,
|
||||
levels: tuple[TraceLevel, ...],
|
||||
ancestor_depth: int | None = 0,
|
||||
) -> TraceLogView | None:
|
||||
parent_id = self._read_parent_id(trace_id)
|
||||
if parent_id is None and not self._trace_exists(trace_id):
|
||||
return None
|
||||
ancestors = self._read_ancestors(parent_id, levels, ancestor_depth)
|
||||
child_ids = self._read_child_ids(trace_id)
|
||||
records = self._read_records(trace_id, levels)
|
||||
return TraceLogView(
|
||||
@@ -22,8 +28,35 @@ class MySqlTraceLogReader(TraceLogReader):
|
||||
parent_id=parent_id,
|
||||
child_ids=tuple(child_ids),
|
||||
records=tuple(records),
|
||||
ancestors=tuple(ancestors),
|
||||
)
|
||||
|
||||
def _read_ancestors(
|
||||
self,
|
||||
parent_id: str | None,
|
||||
levels: tuple[TraceLevel, ...],
|
||||
ancestor_depth: int | None,
|
||||
) -> list[TraceLogView]:
|
||||
if parent_id is None or ancestor_depth == 0:
|
||||
return []
|
||||
remaining_depth = ancestor_depth
|
||||
ancestors: list[TraceLogView] = []
|
||||
current_trace_id = parent_id
|
||||
while current_trace_id is not None and (remaining_depth is None or remaining_depth > 0):
|
||||
current_parent_id = self._read_parent_id(current_trace_id)
|
||||
ancestors.append(
|
||||
TraceLogView(
|
||||
trace_id=current_trace_id,
|
||||
parent_id=current_parent_id,
|
||||
child_ids=tuple(self._read_child_ids(current_trace_id)),
|
||||
records=tuple(self._read_records(current_trace_id, levels)),
|
||||
)
|
||||
)
|
||||
current_trace_id = current_parent_id
|
||||
if remaining_depth is not None:
|
||||
remaining_depth -= 1
|
||||
return ancestors
|
||||
|
||||
def _trace_exists(self, trace_id: str) -> bool:
|
||||
query = "SELECT 1 FROM trace_contexts WHERE trace_id = %s"
|
||||
with self._connection_factory.connect() as connection:
|
||||
|
||||
Reference in New Issue
Block a user