From 647d9ae651ba58d0e4898de1a1917fff2f9ae4fe Mon Sep 17 00:00:00 2001 From: Alexander Presnyakov Date: Fri, 5 Sep 2025 13:15:41 +0000 Subject: [PATCH] Record part of the response into the span --- cmapi/cmapi_server/logging_management.py | 3 +- cmapi/mcs_node_control/models/node_config.py | 5 ++-- cmapi/tracing/trace_tool.py | 31 -------------------- cmapi/tracing/traced_aiohttp.py | 24 +++++++++++++++ cmapi/tracing/traced_session.py | 21 +++++++++++-- cmapi/tracing/traceparent_backend.py | 5 ++++ cmapi/tracing/utils.py | 2 -- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/cmapi/cmapi_server/logging_management.py b/cmapi/cmapi_server/logging_management.py index a3a411583..0d78b57b9 100644 --- a/cmapi/cmapi_server/logging_management.py +++ b/cmapi/cmapi_server/logging_management.py @@ -176,4 +176,5 @@ class JsonFormatter(logging.Formatter): for k, v in record.__dict__.items(): if k not in self.skip_fields: data[k] = v - return json.dumps(data, ensure_ascii=False, sort_keys=True) \ No newline at end of file + # Allow non-serializable extras (e.g., bytes, datetime) to be stringified + return json.dumps(data, ensure_ascii=False, sort_keys=True, default=str) \ No newline at end of file diff --git a/cmapi/mcs_node_control/models/node_config.py b/cmapi/mcs_node_control/models/node_config.py index e83f1e541..0068c35cf 100644 --- a/cmapi/mcs_node_control/models/node_config.py +++ b/cmapi/mcs_node_control/models/node_config.py @@ -7,6 +7,7 @@ import socket from os import mkdir, replace, chown from pathlib import Path from shutil import copyfile +from typing import Iterator from xml.dom import minidom # to pick up pretty printing functionality from lxml import etree @@ -366,12 +367,10 @@ class NodeConfig: return False - def get_network_addresses(self): + def get_network_addresses(self) -> Iterator[str]: """Retrievs the list of the network addresses Generator that yields network interface addresses. - - :rtype: str """ for ni in get_network_interfaces(): for fam in [socket.AF_INET, socket.AF_INET6]: diff --git a/cmapi/tracing/trace_tool.py b/cmapi/tracing/trace_tool.py index 8078f1ced..826830d91 100644 --- a/cmapi/tracing/trace_tool.py +++ b/cmapi/tracing/trace_tool.py @@ -8,36 +8,6 @@ import cherrypy from tracing.tracer import get_tracer - -def _capture_short_json_response(span) -> None: - """If response is JSON and is already serialized (bytes/str), put it into span attrs. - - - Only captures when body is bytes or str (already materialized JSON string). - - Truncates to 500 bytes; records original size - """ - try: - headers = cherrypy.response.headers - content_type = str(headers.get('Content-Type', '')).lower() - if 'application/json' not in content_type: - return - body = cherrypy.response.body # may be bytes/str or an iterable - max_len = 500 - text = None - # Only handle concrete bytes/str bodies (already serialized JSON) - if isinstance(body, (bytes, str)): - if isinstance(body, bytes): - text = body.decode('utf-8', errors='replace') - else: - text = body - # Only set attribute if we could safely materialize text - if text is not None: - value = text[:max_len] - span.set_attribute('http.response.body.size', len(text)) - span.set_attribute('http.response.body', value) - except Exception: - pass - - def _on_request_start() -> None: req = cherrypy.request tracer = get_tracer() @@ -86,7 +56,6 @@ def _on_request_end() -> None: span = getattr(req, "_trace_span", None) if span is not None and status_code is not None: span.set_attribute('http.status_code', status_code) - _capture_short_json_response(span) ctx = getattr(req, "_trace_span_ctx", None) if ctx is not None: try: diff --git a/cmapi/tracing/traced_aiohttp.py b/cmapi/tracing/traced_aiohttp.py index 6c4a279db..e71980c39 100644 --- a/cmapi/tracing/traced_aiohttp.py +++ b/cmapi/tracing/traced_aiohttp.py @@ -1,11 +1,17 @@ """Async sibling of TracedSession.""" from typing import Any +import logging import time import aiohttp from tracing.tracer import get_tracer +# Limit for raw JSON string preview (in characters) +_PREVIEW_MAX_CHARS = 512 + +logger = logging.getLogger("tracer") + class TracedAsyncSession(aiohttp.ClientSession): async def _request( @@ -32,6 +38,7 @@ class TracedAsyncSession(aiohttp.ClientSession): raise else: span.set_attribute("http.status_code", response.status) + await _record_outbound_json_preview(response, span) return response finally: duration_ms = (time.time_ns() - span.start_ns) / 1_000_000.0 @@ -43,3 +50,20 @@ def create_traced_async_session(**kwargs: Any) -> TracedAsyncSession: +async def _record_outbound_json_preview(response: aiohttp.ClientResponse, span) -> None: + """If response is JSON, attach small part of it to span + + We don't use streaming in aiohttp, so reading text is safe here. + """ + try: + content_type = str(response.headers.get('Content-Type', '')).lower() + if 'application/json' not in content_type: + return + text = await response.text() + if text is None: + text = "" + span.set_attribute('http.response.body.size', len(text)) + span.set_attribute('http.response.json', text[:_PREVIEW_MAX_CHARS]) + except Exception: + logger.exception("Could not extract JSON response body") + return None diff --git a/cmapi/tracing/traced_session.py b/cmapi/tracing/traced_session.py index 03f8434aa..49cb1d869 100644 --- a/cmapi/tracing/traced_session.py +++ b/cmapi/tracing/traced_session.py @@ -1,10 +1,16 @@ """Customized requests.Session that automatically traces outbound HTTP calls.""" from typing import Any, Optional +import logging import time import requests -from tracing.tracer import get_tracer +from tracing.tracer import get_tracer, TraceSpan + +# Limit for raw JSON string preview (in characters) +_PREVIEW_MAX_CHARS = 512 + +logger = logging.getLogger("tracer") class TracedSession(requests.Session): @@ -30,6 +36,7 @@ class TracedSession(requests.Session): raise else: span.set_attribute("http.status_code", response.status_code) + _record_outbound_json_preview(response, span) return response finally: duration_ms = (time.time_ns() - span.start_ns) / 1_000_000.0 @@ -46,4 +53,14 @@ def get_traced_session() -> TracedSession: return _default_session - +def _record_outbound_json_preview(response: requests.Response, span: TraceSpan) -> None: + """If response is JSON, attach small part of it to span""" + try: + content_type = str(response.headers.get('Content-Type', '')).lower() + if 'application/json' not in content_type: + return + text = response.text # requests will decode using inferred/declared encoding + span.set_attribute('http.response.body.size', len(text)) + span.set_attribute('http.response.json', text[:_PREVIEW_MAX_CHARS]) + except Exception: + logger.exception("Could not extract JSON response body") diff --git a/cmapi/tracing/traceparent_backend.py b/cmapi/tracing/traceparent_backend.py index 008809a1c..43bf7e474 100644 --- a/cmapi/tracing/traceparent_backend.py +++ b/cmapi/tracing/traceparent_backend.py @@ -2,6 +2,7 @@ import logging import time from typing import Any, Dict, Optional +from mcs_node_control.models.node_config import NodeConfig from tracing.tracer import TracerBackend, TraceSpan from tracing.utils import swallow_exceptions @@ -11,6 +12,10 @@ json_logger = logging.getLogger("json_trace") class TraceparentBackend(TracerBackend): """Default backend that logs span lifecycle and mirrors events/status.""" + def __init__(self): + my_addresses = list(NodeConfig().get_network_addresses()) + logger.info("My addresses: %s", my_addresses) + json_logger.info("my_addresses", extra={"my_addresses": my_addresses}) @swallow_exceptions def on_span_start(self, span: TraceSpan) -> None: diff --git a/cmapi/tracing/utils.py b/cmapi/tracing/utils.py index 26e9d4a0c..e67e003f0 100644 --- a/cmapi/tracing/utils.py +++ b/cmapi/tracing/utils.py @@ -50,5 +50,3 @@ def parse_traceparent(header: str) -> Optional[Tuple[str, str, str]]: except Exception: logger.exception("Failed to parse traceparent: %s", header) return None - -