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) — OTLP/JSON Span Export](#whats-new-2026-06-22--otlpjson-span-export)
- [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)
Expand Down Expand Up @@ -146,6 +147,12 @@

---

## What's new (2026-06-22) — OTLP/JSON Span Export

Export spans the way a collector ingests them. Full reference: [`docs/source/Eng/doc/new_features/v94_features_doc.rst`](docs/source/Eng/doc/new_features/v94_features_doc.rst).

- **`spans_to_otlp` / `attributes_to_otlp` / `write_otlp`** (`AC_spans_to_otlp`): `agent_trace.to_otel` returned flat dicts that aren't valid OTLP/JSON (no resourceSpans/scopeSpans nesting, times not as uint64 strings). This wraps spans in the proper envelope with hex IDs, uint64-string times, and OTLP `KeyValue` attribute encoding — what an OpenTelemetry collector's file exporter reads. Pairs with `trace_context`. Pure-stdlib, deterministic.

## 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).
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) — OTLP/JSON Span 导出](#本次更新-2026-06-22--otlpjson-span-导出)
- [本次更新 (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-会话携带)
Expand Down Expand Up @@ -145,6 +146,12 @@

---

## 本次更新 (2026-06-22) — OTLP/JSON Span 导出

以 collector 摄取的格式导出 span。完整参考:[`docs/source/Zh/doc/new_features/v94_features_doc.rst`](../docs/source/Zh/doc/new_features/v94_features_doc.rst)。

- **`spans_to_otlp` / `attributes_to_otlp` / `write_otlp`**(`AC_spans_to_otlp`):`agent_trace.to_otel` 返回扁平 dict,并非有效 OTLP/JSON(没有 resourceSpans/scopeSpans 嵌套、时间不是 uint64 字符串)。本功能把 span 包进正确封套,含 hex ID、uint64 字符串时间,以及 OTLP `KeyValue` 属性编码 —— OpenTelemetry collector file exporter 读取的格式。与 `trace_context` 搭配。纯标准库、确定。

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

每次执行一行宽事件,并带 trace 关联。完整参考:[`docs/source/Zh/doc/new_features/v93_features_doc.rst`](../docs/source/Zh/doc/new_features/v93_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) — OTLP/JSON Span 匯出](#本次更新-2026-06-22--otlpjson-span-匯出)
- [本次更新 (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-工作階段攜帶)
Expand Down Expand Up @@ -145,6 +146,12 @@

---

## 本次更新 (2026-06-22) — OTLP/JSON Span 匯出

以 collector 攝取的格式匯出 span。完整參考:[`docs/source/Zh/doc/new_features/v94_features_doc.rst`](../docs/source/Zh/doc/new_features/v94_features_doc.rst)。

- **`spans_to_otlp` / `attributes_to_otlp` / `write_otlp`**(`AC_spans_to_otlp`):`agent_trace.to_otel` 回傳扁平 dict,並非有效 OTLP/JSON(沒有 resourceSpans/scopeSpans 巢狀、時間不是 uint64 字串)。本功能把 span 包進正確封套,含 hex ID、uint64 字串時間,以及 OTLP `KeyValue` 屬性編碼 —— OpenTelemetry collector file exporter 讀取的格式。與 `trace_context` 搭配。純標準函式庫、具決定性。

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

每次執行一行寬事件,並帶 trace 關聯。完整參考:[`docs/source/Zh/doc/new_features/v93_features_doc.rst`](../docs/source/Zh/doc/new_features/v93_features_doc.rst)。
Expand Down
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v94_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
OTLP/JSON Span Export
=====================

``agent_trace.to_otel`` returns flat span dicts that are not valid OTLP/JSON
(no ``resourceSpans`` / ``scopeSpans`` nesting, no proper attribute encoding,
times not as uint64 strings). This shapes a list of spans into the envelope an
OpenTelemetry collector ingests directly via its file exporter.

Pure standard library (``json``); imports no ``PySide6``. Times are supplied by
the caller (no wall clock), so the envelope is byte-stable and CI-testable.

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

.. code-block:: python

from je_auto_control import spans_to_otlp, write_otlp

spans = [{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"name": "run_suite",
"start_unix_nano": started_ns, "end_unix_nano": ended_ns,
"attributes": {"ok": True, "cases": 12},
}]
payload = spans_to_otlp(spans, resource_attrs={"service.name": "autocontrol"})
write_otlp(payload, "trace.otlp.json")

``spans_to_otlp`` wraps spans in the ``resourceSpans → scopeSpans → spans``
structure: trace/span IDs stay hex, times become uint64 strings, and attributes
are encoded as OTLP ``KeyValue`` entries (``stringValue`` / ``intValue`` /
``boolValue`` / ``doubleValue``). ``attributes_to_otlp`` exposes that attribute
conversion, and ``write_otlp`` writes the payload as JSON. The result is what an
OpenTelemetry collector's file exporter reads — pairing with ``trace_context``
for the IDs and ``agent_trace`` for the span data.

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

``AC_spans_to_otlp`` wraps ``spans`` (with optional ``resource_attrs``) into
``{payload}``. It is exposed as the MCP tool ``ac_spans_to_otlp`` 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 @@ -116,6 +116,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v91_features_doc
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/new_features/v94_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
38 changes: 38 additions & 0 deletions docs/source/Zh/doc/new_features/v94_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
OTLP/JSON Span 匯出
==================

``agent_trace.to_otel`` 回傳的是扁平 span dict,並非有效的 OTLP/JSON(沒有 ``resourceSpans`` /
``scopeSpans`` 巢狀、屬性未正確編碼、時間不是 uint64 字串)。本功能把一串 span 塑形成 OpenTelemetry
collector 可透過 file exporter 直接攝取的封套。

純標準函式庫(``json``);不匯入 ``PySide6``。時間由呼叫端提供(不使用 wall clock),因此封套位元組穩定、
可於 CI 測試。

無頭 API
--------

.. code-block:: python

from je_auto_control import spans_to_otlp, write_otlp

spans = [{
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"name": "run_suite",
"start_unix_nano": started_ns, "end_unix_nano": ended_ns,
"attributes": {"ok": True, "cases": 12},
}]
payload = spans_to_otlp(spans, resource_attrs={"service.name": "autocontrol"})
write_otlp(payload, "trace.otlp.json")

``spans_to_otlp`` 把 span 包進 ``resourceSpans → scopeSpans → spans`` 結構:trace/span ID 維持 hex、
時間轉成 uint64 字串、屬性編碼為 OTLP ``KeyValue``(``stringValue`` / ``intValue`` / ``boolValue`` /
``doubleValue``)。``attributes_to_otlp`` 公開該屬性轉換,``write_otlp`` 把 payload 寫成 JSON。結果即為
OpenTelemetry collector file exporter 讀取的格式 —— 與 ``trace_context``(提供 ID)及 ``agent_trace``
(提供 span 資料)搭配。

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

``AC_spans_to_otlp`` 把 ``spans``(以及選用的 ``resource_attrs``)包成 ``{payload}``。它以 MCP 工具
``ac_spans_to_otlp`` 以及 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 @@ -116,6 +116,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v91_features_doc
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/new_features/v94_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 @@ -450,6 +450,10 @@
from je_auto_control.utils.canonical_log import (
CanonicalLogLine, JSONLogFormatter, bind_trace_context,
)
# OTLP/JSON span export (resourceSpans envelope)
from je_auto_control.utils.otlp_export import (
attributes_to_otlp, spans_to_otlp, write_otlp,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -976,6 +980,7 @@ def start_autocontrol_gui(*args, **kwargs):
"Baggage", "extract_baggage", "format_baggage", "inject_baggage",
"parse_baggage",
"CanonicalLogLine", "JSONLogFormatter", "bind_trace_context",
"attributes_to_otlp", "spans_to_otlp", "write_otlp",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
12 changes: 12 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,18 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Build a canonical wide-event log line (rendered as JSON).",
))
specs.append(CommandSpec(
"AC_spans_to_otlp", "Report", "OTLP: Export Spans",
fields=(
FieldSpec("spans", FieldType.STRING,
placeholder='[{"trace_id": "...", "span_id": "...", '
'"name": "step", "start_unix_nano": 1, '
'"end_unix_nano": 2}]'),
FieldSpec("resource_attrs", FieldType.STRING, optional=True,
placeholder='{"service.name": "autocontrol"}'),
),
description="Wrap spans in an OTLP/JSON resourceSpans envelope.",
))
specs.append(CommandSpec(
"AC_resolve_ref", "Security", "Secret Ref: Resolve",
fields=(
Expand Down
12 changes: 12 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3137,6 +3137,17 @@ def _canonical_log(fields: Any) -> Dict[str, Any]:
return {"line": line.to_dict(), "json": line.render()}


def _spans_to_otlp(spans: Any, resource_attrs: Any = None) -> Dict[str, Any]:
"""Adapter: wrap spans in an OTLP/JSON resourceSpans envelope."""
import json
from je_auto_control.utils.otlp_export import spans_to_otlp
if isinstance(spans, str):
spans = json.loads(spans)
if isinstance(resource_attrs, str):
resource_attrs = json.loads(resource_attrs)
return {"payload": spans_to_otlp(spans, resource_attrs=resource_attrs)}


def _baggage_format(items: Any) -> Dict[str, Any]:
"""Adapter: serialise an items dict into a W3C baggage {header}."""
import json
Expand Down Expand Up @@ -4389,6 +4400,7 @@ def __init__(self):
"AC_baggage_parse": _baggage_parse,
"AC_baggage_format": _baggage_format,
"AC_canonical_log": _canonical_log,
"AC_spans_to_otlp": _spans_to_otlp,
"AC_resolve_ref": _resolve_ref,
"AC_resolve_refs": _resolve_refs,
"AC_redact_config": _redact_config,
Expand Down
22 changes: 20 additions & 2 deletions je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3731,6 +3731,24 @@ def secret_ref_tools() -> List[MCPTool]:
]


def otlp_export_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_spans_to_otlp",
description=("Wrap 'spans' (each {trace_id, span_id, name, "
"start_unix_nano, end_unix_nano, attributes?}) in an "
"OTLP/JSON resourceSpans envelope; optional "
"'resource_attrs'. Returns {payload}."),
input_schema=schema(
{"spans": {"type": "array"},
"resource_attrs": {"type": "object"}},
["spans"]),
handler=h.spans_to_otlp,
annotations=READ_ONLY,
),
]


def canonical_log_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5326,8 +5344,8 @@ 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, canonical_log_tools, secret_ref_tools,
config_redaction_tools,
trace_context_tools, baggage_tools, canonical_log_tools, otlp_export_tools,
secret_ref_tools, config_redaction_tools,
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
dataset_diff_tools, referential_tools, link_header_tools, multipart_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 @@ -1741,6 +1741,11 @@ def canonical_log(fields):
return _canonical_log(fields)


def spans_to_otlp(spans, resource_attrs=None):
from je_auto_control.utils.executor.action_executor import _spans_to_otlp
return _spans_to_otlp(spans, resource_attrs)


def resolve_ref(ref):
from je_auto_control.utils.executor.action_executor import _resolve_ref
return _resolve_ref(ref)
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/otlp_export/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""OTLP/JSON span export for AutoControl."""
from je_auto_control.utils.otlp_export.otlp_export import (
attributes_to_otlp, spans_to_otlp, write_otlp,
)

__all__ = ["attributes_to_otlp", "spans_to_otlp", "write_otlp"]
75 changes: 75 additions & 0 deletions je_auto_control/utils/otlp_export/otlp_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Render spans into the OTLP/JSON ``resourceSpans`` envelope.

``agent_trace.to_otel`` returns flat span dicts that are not valid OTLP/JSON
(no ``resourceSpans`` / ``scopeSpans`` nesting, no proper attribute encoding,
times not as uint64 strings). This shapes a list of spans into the envelope an
OpenTelemetry collector ingests directly via its file exporter.

Pure standard library (``json``); imports no ``PySide6``. Times are supplied by
the caller (no wall clock), so the envelope is byte-stable and CI-testable.
"""
import json
from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional, Sequence


def _attr_value(value: Any) -> Dict[str, Any]:
if isinstance(value, bool):
return {"boolValue": value}
if isinstance(value, int):
return {"intValue": str(value)} # int64 encoded as string
if isinstance(value, float):
return {"doubleValue": value}
return {"stringValue": str(value)}


def attributes_to_otlp(attributes: Optional[Mapping[str, Any]]
) -> List[Dict[str, Any]]:
"""Convert a plain attribute dict to an OTLP KeyValue list."""
return [{"key": str(key), "value": _attr_value(value)}
for key, value in (attributes or {}).items()]


def _span_to_otlp(span: Mapping[str, Any]) -> Dict[str, Any]:
out: Dict[str, Any] = {
"traceId": span["trace_id"],
"spanId": span["span_id"],
"name": span.get("name", ""),
"kind": int(span.get("kind", 1)),
"startTimeUnixNano": str(span.get("start_unix_nano", 0)),
"endTimeUnixNano": str(span.get("end_unix_nano", 0)),
"attributes": attributes_to_otlp(span.get("attributes")),
}
if span.get("parent_span_id"):
out["parentSpanId"] = span["parent_span_id"]
return out


def spans_to_otlp(spans: Sequence[Mapping[str, Any]], *,
resource_attrs: Optional[Mapping[str, Any]] = None,
scope_name: str = "je_auto_control",
scope_version: str = "") -> Dict[str, Any]:
"""Wrap ``spans`` in an OTLP/JSON ``resourceSpans`` envelope.

Each span is a dict with ``trace_id`` / ``span_id`` (hex), ``name``,
``start_unix_nano`` / ``end_unix_nano`` and optional ``attributes`` /
``parent_span_id`` / ``kind``.
"""
scope: Dict[str, Any] = {"name": scope_name}
if scope_version:
scope["version"] = scope_version
return {"resourceSpans": [{
"resource": {"attributes": attributes_to_otlp(resource_attrs)},
"scopeSpans": [{
"scope": scope,
"spans": [_span_to_otlp(span) for span in spans],
}],
}]}


def write_otlp(payload: Mapping[str, Any], path: str) -> str:
"""Write an OTLP payload to ``path`` as JSON; return the path."""
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return str(out)
Loading
Loading