Skip to content
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-22) — Canonical Log Lines & Structured Logging](#whats-new-2026-06-22--canonical-log-lines--structured-logging)
- [What's new (2026-06-22) — Conditional HTTP Requests & Cache Validators](#whats-new-2026-06-22--conditional-http-requests--cache-validators)
- [What's new (2026-06-22) — Cookie Jar (HTTP Session Carry)](#whats-new-2026-06-22--cookie-jar-http-session-carry)
- [What's new (2026-06-22) — HTTP Content Negotiation & Decompression](#whats-new-2026-06-22--http-content-negotiation--decompression)
Expand Down Expand Up @@ -145,6 +146,12 @@

---

## What's new (2026-06-22) — Canonical Log Lines & Structured Logging

One wide event per run, with trace correlation. Full reference: [`docs/source/Eng/doc/new_features/v93_features_doc.rst`](docs/source/Eng/doc/new_features/v93_features_doc.rst).

- **`CanonicalLogLine` / `JSONLogFormatter` / `bind_trace_context`** (`AC_canonical_log`): `logging_instance` emits a fixed pipe-delimited string with no JSON and no trace/span fields. This adds a Stripe-style canonical log line (field accumulator + `timer` with injectable clock) and a JSON `logging.Formatter` that carries `trace_id`/`span_id` — the log-trace correlation counterpart to `trace_context`. Pure-stdlib, deterministic.

## What's new (2026-06-22) — Conditional HTTP Requests & Cache Validators

Skip re-downloading unchanged resources (ETag / 304). Full reference: [`docs/source/Eng/doc/new_features/v92_features_doc.rst`](docs/source/Eng/doc/new_features/v92_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-22) — 标准日志行与结构化日志](#本次更新-2026-06-22--标准日志行与结构化日志)
- [本次更新 (2026-06-22) — 条件式 HTTP 请求与缓存验证子](#本次更新-2026-06-22--条件式-http-请求与缓存验证子)
- [本次更新 (2026-06-22) — Cookie Jar(HTTP 会话携带)](#本次更新-2026-06-22--cookie-jarhttp-会话携带)
- [本次更新 (2026-06-22) — HTTP 内容协商与解压](#本次更新-2026-06-22--http-内容协商与解压)
Expand Down Expand Up @@ -144,6 +145,12 @@

---

## 本次更新 (2026-06-22) — 标准日志行与结构化日志

每次执行一行宽事件,并带 trace 关联。完整参考:[`docs/source/Zh/doc/new_features/v93_features_doc.rst`](../docs/source/Zh/doc/new_features/v93_features_doc.rst)。

- **`CanonicalLogLine` / `JSONLogFormatter` / `bind_trace_context`**(`AC_canonical_log`):`logging_instance` 输出固定的管线分隔字符串,没有 JSON 也没有 trace/span 字段。本功能加入 Stripe 风格的标准日志行(字段累积器 + 可注入时钟的 `timer`)以及携带 `trace_id`/`span_id` 的 JSON `logging.Formatter` —— 与 `trace_context` 对应的 log-trace 关联。纯标准库、确定。

## 本次更新 (2026-06-22) — 条件式 HTTP 请求与缓存验证子

跳过重新下载未变更的资源(ETag / 304)。完整参考:[`docs/source/Zh/doc/new_features/v92_features_doc.rst`](../docs/source/Zh/doc/new_features/v92_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-22) — 標準日誌行與結構化日誌](#本次更新-2026-06-22--標準日誌行與結構化日誌)
- [本次更新 (2026-06-22) — 條件式 HTTP 請求與快取驗證子](#本次更新-2026-06-22--條件式-http-請求與快取驗證子)
- [本次更新 (2026-06-22) — Cookie Jar(HTTP 工作階段攜帶)](#本次更新-2026-06-22--cookie-jarhttp-工作階段攜帶)
- [本次更新 (2026-06-22) — HTTP 內容協商與解壓縮](#本次更新-2026-06-22--http-內容協商與解壓縮)
Expand Down Expand Up @@ -144,6 +145,12 @@

---

## 本次更新 (2026-06-22) — 標準日誌行與結構化日誌

每次執行一行寬事件,並帶 trace 關聯。完整參考:[`docs/source/Zh/doc/new_features/v93_features_doc.rst`](../docs/source/Zh/doc/new_features/v93_features_doc.rst)。

- **`CanonicalLogLine` / `JSONLogFormatter` / `bind_trace_context`**(`AC_canonical_log`):`logging_instance` 輸出固定的管線分隔字串,沒有 JSON 也沒有 trace/span 欄位。本功能加入 Stripe 風格的標準日誌行(欄位累積器 + 可注入時鐘的 `timer`)以及攜帶 `trace_id`/`span_id` 的 JSON `logging.Formatter` —— 與 `trace_context` 對應的 log-trace 關聯。純標準函式庫、具決定性。

## 本次更新 (2026-06-22) — 條件式 HTTP 請求與快取驗證子

略過重新下載未變更的資源(ETag / 304)。完整參考:[`docs/source/Zh/doc/new_features/v92_features_doc.rst`](../docs/source/Zh/doc/new_features/v92_features_doc.rst)。
Expand Down
45 changes: 45 additions & 0 deletions docs/source/Eng/doc/new_features/v93_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Canonical Log Lines & Structured Logging
========================================

``logging_instance`` emits a fixed pipe-delimited human string with no JSON
option and no trace/span fields, but OTel log-trace correlation needs
``trace_id`` / ``span_id`` on each record. This adds a Stripe-style canonical
log line (one wide field-bag emitted per run) and a JSON ``logging.Formatter``
that carries trace context.

Pure standard library (``json`` / ``logging``); imports no ``PySide6``. The
timer clock is injectable, so durations are deterministic in CI.

Headless API
------------

.. code-block:: python

from je_auto_control import (
CanonicalLogLine, bind_trace_context, JSONLogFormatter,
new_root_context,
)

line = CanonicalLogLine({"event": "run_suite"})
bind_trace_context(line, new_root_context())
with line.timer("execute"):
run()
line.add("ok", True).emit(logger.info) # one wide line per run

# structured logging with trace correlation:
handler.setFormatter(JSONLogFormatter())

``CanonicalLogLine`` accumulates request-scoped fields (``add`` / ``update``),
times a block into ``{name}_ms`` via ``timer`` (injectable clock), and
``render`` / ``emit`` the wide line as JSON. ``bind_trace_context`` attaches a
``SpanContext``'s ``trace_id`` / ``span_id``. ``JSONLogFormatter`` is a
``logging.Formatter`` that emits one JSON object per record including
``trace_id`` / ``span_id`` and any ``extra=`` fields — the log-trace correlation
counterpart to ``trace_context``.

Executor command
----------------

``AC_canonical_log`` builds a canonical line from a ``fields`` object and returns
``{line, json}``. It is exposed as the MCP tool ``ac_canonical_log`` and as a
Script Builder command under **Report**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v90_features_doc
doc/new_features/v91_features_doc
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
39 changes: 39 additions & 0 deletions docs/source/Zh/doc/new_features/v93_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
標準日誌行與結構化日誌
====================

``logging_instance`` 輸出固定的管線分隔人類字串,沒有 JSON 選項也沒有 trace/span 欄位,但 OTel 的
log-trace 關聯需要每筆記錄都帶 ``trace_id`` / ``span_id``。本功能加入 Stripe 風格的標準日誌行(每次執行
輸出一個寬欄位包)以及一個攜帶 trace 脈絡的 JSON ``logging.Formatter``。

純標準函式庫(``json`` / ``logging``);不匯入 ``PySide6``。計時器時鐘可注入,因此時長在 CI 中具決定性。

無頭 API
--------

.. code-block:: python

from je_auto_control import (
CanonicalLogLine, bind_trace_context, JSONLogFormatter,
new_root_context,
)

line = CanonicalLogLine({"event": "run_suite"})
bind_trace_context(line, new_root_context())
with line.timer("execute"):
run()
line.add("ok", True).emit(logger.info) # 每次執行一行寬事件

# 帶 trace 關聯的結構化日誌:
handler.setFormatter(JSONLogFormatter())

``CanonicalLogLine`` 累積請求範圍的欄位(``add`` / ``update``),透過 ``timer``(可注入時鐘)把一段區塊
計入 ``{name}_ms``,並以 ``render`` / ``emit`` 將寬事件行輸出為 JSON。``bind_trace_context`` 附上
``SpanContext`` 的 ``trace_id`` / ``span_id``。``JSONLogFormatter`` 是一個 ``logging.Formatter``,每筆記錄
輸出一個 JSON 物件,包含 ``trace_id`` / ``span_id`` 與任何 ``extra=`` 欄位 —— 與 ``trace_context`` 對應的
log-trace 關聯。

執行器命令
----------

``AC_canonical_log`` 從 ``fields`` 物件建立標準日誌行,回傳 ``{line, json}``。它以 MCP 工具
``ac_canonical_log`` 以及 Script Builder 中 **Report** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v90_features_doc
doc/new_features/v91_features_doc
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,10 @@
from je_auto_control.utils.baggage import (
Baggage, extract_baggage, format_baggage, inject_baggage, parse_baggage,
)
# Canonical (wide-event) log lines + structured JSON logging
from je_auto_control.utils.canonical_log import (
CanonicalLogLine, JSONLogFormatter, bind_trace_context,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -971,6 +975,7 @@ def start_autocontrol_gui(*args, **kwargs):
"parse_tracestate",
"Baggage", "extract_baggage", "format_baggage", "inject_baggage",
"parse_baggage",
"CanonicalLogLine", "JSONLogFormatter", "bind_trace_context",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
8 changes: 8 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,14 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Serialise items into a percent-encoded baggage header.",
))
specs.append(CommandSpec(
"AC_canonical_log", "Report", "Canonical Log: Build Line",
fields=(
FieldSpec("fields", FieldType.STRING,
placeholder='{"event": "run", "ok": true, "ms": 42}'),
),
description="Build a canonical wide-event log line (rendered as JSON).",
))
specs.append(CommandSpec(
"AC_resolve_ref", "Security", "Secret Ref: Resolve",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/canonical_log/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Canonical log lines and structured JSON logging for AutoControl."""
from je_auto_control.utils.canonical_log.canonical_log import (
CanonicalLogLine, JSONLogFormatter, bind_trace_context,
)

__all__ = ["CanonicalLogLine", "JSONLogFormatter", "bind_trace_context"]
84 changes: 84 additions & 0 deletions je_auto_control/utils/canonical_log/canonical_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Canonical (wide-event) log lines and structured JSON log formatting.

``logging_instance`` emits a fixed pipe-delimited human string with no JSON
option and no trace/span fields, but OTel log-trace correlation needs
``trace_id`` / ``span_id`` on each record. This adds a Stripe-style canonical
log line (one wide field-bag emitted per run) and a JSON ``logging.Formatter``
that carries trace context.

Pure standard library (``json`` / ``logging``); imports no ``PySide6``. The
timer clock is injectable, so durations are deterministic in CI.
"""
import json
import logging
import time
from contextlib import contextmanager
from typing import Any, Callable, Dict, Iterator, Mapping, Optional

_STANDARD = frozenset(vars(logging.makeLogRecord({})))


class CanonicalLogLine:
"""A request-scoped accumulator emitting one wide log line."""

def __init__(self, fields: Optional[Mapping[str, Any]] = None, *,
clock: Callable[[], float] = time.monotonic) -> None:
self._fields: Dict[str, Any] = dict(fields or {})
self._clock = clock

def add(self, key: str, value: Any) -> "CanonicalLogLine":
"""Add or overwrite one field; returns self for chaining."""
self._fields[str(key)] = value
return self

def update(self, mapping: Mapping[str, Any]) -> "CanonicalLogLine":
"""Merge a mapping of fields."""
for key, value in mapping.items():
self._fields[str(key)] = value
return self

@contextmanager
def timer(self, name: str) -> Iterator[None]:
"""Time a block, recording ``{name}_ms`` on completion."""
start = self._clock()
try:
yield
finally:
self._fields[f"{name}_ms"] = round((self._clock() - start) * 1000, 3)

def to_dict(self) -> Dict[str, Any]:
"""Return the accumulated fields."""
return dict(self._fields)

def render(self) -> str:
"""Render the line as a single JSON object (sorted keys)."""
return json.dumps(self._fields, sort_keys=True, default=str)

def emit(self, sink: Callable[[Dict[str, Any]], Any]) -> "CanonicalLogLine":
"""Send the field dict to ``sink`` (e.g. a logger or a list append)."""
sink(self.to_dict())
return self


def bind_trace_context(line: CanonicalLogLine,
context: Any) -> CanonicalLogLine:
"""Attach ``trace_id`` / ``span_id`` from a SpanContext to ``line``."""
return line.add("trace_id", context.trace_id).add("span_id",
context.span_id)


class JSONLogFormatter(logging.Formatter):
"""A ``logging.Formatter`` that emits one JSON object per record.

Includes ``trace_id`` / ``span_id`` (and any non-standard ``extra=`` fields)
when present, for OTel log-trace correlation.
"""

def format(self, record: logging.LogRecord) -> str:
payload: Dict[str, Any] = {"level": record.levelname,
"logger": record.name,
"message": record.getMessage()}
for key, value in vars(record).items():
if key not in _STANDARD and not key.startswith("_"):
payload[key] = value
return json.dumps(payload, default=str)
11 changes: 11 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3127,6 +3127,16 @@ def _baggage_parse(header: str) -> Dict[str, Any]:
return {"items": parse_baggage(header).to_dict()}


def _canonical_log(fields: Any) -> Dict[str, Any]:
"""Adapter: build a canonical log line from a fields dict."""
import json
from je_auto_control.utils.canonical_log import CanonicalLogLine
if isinstance(fields, str):
fields = json.loads(fields)
line = CanonicalLogLine(fields)
return {"line": line.to_dict(), "json": line.render()}


def _baggage_format(items: Any) -> Dict[str, Any]:
"""Adapter: serialise an items dict into a W3C baggage {header}."""
import json
Expand Down Expand Up @@ -4378,6 +4388,7 @@ def __init__(self):
"AC_trace_extract": _trace_extract,
"AC_baggage_parse": _baggage_parse,
"AC_baggage_format": _baggage_format,
"AC_canonical_log": _canonical_log,
"AC_resolve_ref": _resolve_ref,
"AC_resolve_refs": _resolve_refs,
"AC_redact_config": _redact_config,
Expand Down
15 changes: 14 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3731,6 +3731,19 @@ def secret_ref_tools() -> List[MCPTool]:
]


def canonical_log_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_canonical_log",
description=("Build a canonical (wide-event) log line from a "
"'fields' object. Returns {line, json}."),
input_schema=schema({"fields": {"type": "object"}}, ["fields"]),
handler=h.canonical_log,
annotations=READ_ONLY,
),
]


def baggage_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5313,7 +5326,7 @@ def media_assert_tools() -> List[MCPTool]:
search_index_tools, stats_tools, recurrence_tools, text_diff_tools,
feature_flag_tools, provenance_tools, json_contract_tools, chaos_tools,
slo_tools, percentiles_tools, bulkhead_tools, http_cassette_tools,
trace_context_tools, baggage_tools, secret_ref_tools,
trace_context_tools, baggage_tools, canonical_log_tools, secret_ref_tools,
config_redaction_tools,
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,11 @@ def baggage_format(items):
return _baggage_format(items)


def canonical_log(fields):
from je_auto_control.utils.executor.action_executor import _canonical_log
return _canonical_log(fields)


def resolve_ref(ref):
from je_auto_control.utils.executor.action_executor import _resolve_ref
return _resolve_ref(ref)
Expand Down
Loading
Loading