This is an automated email from the ASF dual-hosted git repository.

hello-stephen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 74d11cd21a8 [fix](ci) Migrate code review tracing to OTLP (#67413)
74d11cd21a8 is described below

commit 74d11cd21a87dcc943871586f5bab62ac98332fc
Author: shuke <[email protected]>
AuthorDate: Thu Sep 3 18:33:36 2026 +0800

    [fix](ci) Migrate code review tracing to OTLP (#67413)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #67416
    
    Problem Summary:
    
    The current Litefuse deployment rejects the legacy `trace-create`,
    `span-create`, and `generation-create` events sent to
    `/api/public/ingestion`. The Code Review workflow itself remained green
    because trace recording is non-blocking, but the Litefuse step returned
    HTTP 400 and new review traces stopped appearing after August 28.
    
    This PR is the minimal functional fix. It converts the existing
    in-memory review event model to OTLP/HTTP JSON spans and sends them to
    `/api/public/otel/v1/traces` with `x-langfuse-ingestion-version: 4`. It
    preserves the trace/span hierarchy, root and child observation
    input/output, trace metadata and tags, model usage, complete-envelope
    payload sizing, bounded HTTP 413 recovery, failed-turn status messages,
    OTLP partial-success detection, and cursor-paginated v2 readback.
    
    This PR does not change review decisions, token handling, workflow
    triggers, or Doris product code. Transient HTTP retry/backoff, complete
    read-back count/ID validation, duplicate detection, and large-trace
    chunking optimization remain separated into #67416.
---
 .github/scripts/emit_litefuse_otel_io.py      | 507 ++++++++++++++++++---
 .github/scripts/test_emit_litefuse_otel_io.py | 633 ++++++++++++++++++++++++++
 2 files changed, 1065 insertions(+), 75 deletions(-)

diff --git a/.github/scripts/emit_litefuse_otel_io.py 
b/.github/scripts/emit_litefuse_otel_io.py
index 4ef45f08879..463876a89e8 100644
--- a/.github/scripts/emit_litefuse_otel_io.py
+++ b/.github/scripts/emit_litefuse_otel_io.py
@@ -19,6 +19,7 @@
 import argparse
 import base64
 from datetime import datetime, timedelta, timezone
+import hashlib
 import json
 import os
 import secrets
@@ -448,7 +449,9 @@ def build_ingestion_payload(args, input_text, output_text, 
events):
             ),
         }
     else:
-        turn_body["statusMessage"] = json_attr(turn_payload)
+        turn_body["statusMessage"] = truncate_text(
+            json_attr(turn_payload), args.max_json_chars
+        )
 
     batch.append(ingestion_event("generation-create", iso_from_ns(now + 
1_000_000), turn_body))
 
@@ -924,7 +927,7 @@ def build_subagent_session_payload(args, session_path, 
session_events):
                 "name": "codex.subagent.review",
                 "startTime": first_timestamp,
                 "endTime": root_end,
-                "input": {"session_file": session_path, "thread_id": 
thread_id},
+                "input": {"prompt": trace_input},
                 "output": {"final_message": trace_output},
                 "environment": args.environment,
                 "metadata": trace_metadata,
@@ -1024,11 +1027,18 @@ def compact_context_event(event, max_chars):
     return compact
 
 
-def shrink_event_for_payload(event, max_payload_bytes):
+def shrink_event_for_payload(
+    event, max_payload_bytes, payload_bytes=None, minimize=False
+):
     shrunk = json.loads(json.dumps(event, ensure_ascii=False))
     body = shrunk.get("body") if isinstance(shrunk.get("body"), dict) else {}
     event_name = body.get("name")
 
+    def measured_payload_bytes(candidate):
+        if payload_bytes is not None:
+            return payload_bytes(candidate)
+        return json_payload_bytes({"batch": [candidate]})
+
     def candidate_with_limits(max_chars, max_context_events=None):
         candidate = json.loads(json.dumps(shrunk, ensure_ascii=False))
         candidate_body = candidate.get("body") if 
isinstance(candidate.get("body"), dict) else {}
@@ -1076,28 +1086,55 @@ def shrink_event_for_payload(event, max_payload_bytes):
         metadata = candidate_body.get("metadata")
         if metadata not in (None, ""):
             candidate_body["metadata"] = truncate_json(metadata, max_chars)
+        status_message = candidate_body.get("statusMessage")
+        if status_message not in (None, ""):
+            candidate_body["statusMessage"] = truncate_text(
+                str(status_message), max_chars
+            )
         return candidate
 
+    if minimize:
+        # This policy floor keeps adaptive 413 retries above the fixed OTLP
+        # envelope while removing every field that the exporter can shrink.
+        candidate = candidate_with_limits(10, 0)
+        if measured_payload_bytes(candidate) < measured_payload_bytes(event):
+            return candidate
+        return event
+
+    def maximize_candidate(max_chars, max_context_events=None):
+        best = candidate_with_limits(max_chars, max_context_events)
+        lower = max_chars + 1
+        upper = max_payload_bytes
+        while lower <= upper:
+            middle = (lower + upper) // 2
+            candidate = candidate_with_limits(middle, max_context_events)
+            if measured_payload_bytes(candidate) <= max_payload_bytes:
+                best = candidate
+                lower = middle + 1
+            else:
+                upper = middle - 1
+        return best
+
     for max_chars in (2_000, 1_000, 500, 200, 80):
         candidate = candidate_with_limits(max_chars)
 
-        if json_payload_bytes({"batch": [candidate]}) <= max_payload_bytes:
-            return candidate
+        if measured_payload_bytes(candidate) <= max_payload_bytes:
+            return maximize_candidate(max_chars)
 
     for max_context_events in (50, 20, 10, 5, 2, 1, 0):
         for max_chars in (80, 40, 20, 10):
             candidate = candidate_with_limits(max_chars, max_context_events)
-            if json_payload_bytes({"batch": [candidate]}) <= max_payload_bytes:
-                return candidate
+            if measured_payload_bytes(candidate) <= max_payload_bytes:
+                return maximize_candidate(max_chars, max_context_events)
 
     raise RuntimeError(
         "Litefuse ingestion event is too large after truncation: "
-        f"{json_payload_bytes({'batch': [event]})} bytes > {max_payload_bytes} 
bytes; "
+        f"{measured_payload_bytes(event)} bytes > {max_payload_bytes} bytes; "
         f"type={event.get('type')}, name={event_name}"
     )
 
 
-def chunk_payload(payload, max_payload_bytes):
+def chunk_payload(payload, max_payload_bytes, shrink_oversized=True):
     batch = payload.get("batch") or []
     chunks = []
     current = []
@@ -1106,7 +1143,7 @@ def chunk_payload(payload, max_payload_bytes):
     for event in batch:
         event_payload = {"batch": [event]}
         event_bytes = json_payload_bytes(event_payload)
-        if event_bytes > max_payload_bytes:
+        if event_bytes > max_payload_bytes and shrink_oversized:
             event = shrink_event_for_payload(event, max_payload_bytes)
             event_payload = {"batch": [event]}
             event_bytes = json_payload_bytes(event_payload)
@@ -1126,6 +1163,280 @@ def chunk_payload(payload, max_payload_bytes):
     return chunks
 
 
+def otel_id(value, byte_count):
+    expected_length = byte_count * 2
+    normalized = str(value or "").lower()
+    if len(normalized) == expected_length and all(
+        char in "0123456789abcdef" for char in normalized
+    ):
+        return normalized
+    return hashlib.blake2b(normalized.encode(), 
digest_size=byte_count).hexdigest()
+
+
+def unix_nanos(timestamp):
+    parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
+    if parsed.tzinfo is None:
+        raise ValueError(f"OpenTelemetry timestamp has no timezone: 
{timestamp}")
+    delta = parsed.astimezone(timezone.utc) - datetime(1970, 1, 1, 
tzinfo=timezone.utc)
+    seconds = delta.days * 86_400 + delta.seconds
+    return str(seconds * 1_000_000_000 + delta.microseconds * 1_000)
+
+
+def otel_any_value(value):
+    if isinstance(value, bool):
+        return {"boolValue": value}
+    if isinstance(value, int):
+        return {"intValue": str(value)}
+    if isinstance(value, float):
+        return {"doubleValue": value}
+    if isinstance(value, str):
+        return {"stringValue": value}
+    if isinstance(value, list) and all(
+        isinstance(item, (bool, int, float, str)) for item in value
+    ):
+        return {"arrayValue": {"values": [otel_any_value(item) for item in 
value]}}
+    return {"stringValue": json_attr(value)}
+
+
+def otel_attributes(values):
+    return [
+        {"key": key, "value": otel_any_value(value)}
+        for key, value in values.items()
+        if value is not None
+    ]
+
+
+def serialized_otel_value(value):
+    if isinstance(value, str):
+        return value
+    return json_attr(value)
+
+
+def metadata_otel_attributes(prefix, metadata):
+    if not isinstance(metadata, dict):
+        return {prefix: serialized_otel_value(metadata)} if metadata is not 
None else {}
+    return {
+        f"{prefix}.{key}": (
+            value if isinstance(value, (str, int)) else 
serialized_otel_value(value)
+        )
+        for key, value in metadata.items()
+        if value is not None
+    }
+
+
+def trace_body_from_payload(payload):
+    for event in payload.get("batch") or []:
+        if event.get("type") == "trace-create" and 
isinstance(event.get("body"), dict):
+            return event["body"]
+    raise RuntimeError("Litefuse payload is missing its trace-create context 
event")
+
+
+def legacy_event_to_otel_span(event, trace_body):
+    event_type = event.get("type")
+    if event_type not in ("span-create", "generation-create"):
+        raise RuntimeError(f"Unsupported trace event for OTLP conversion: 
{event_type}")
+    body = event.get("body") if isinstance(event.get("body"), dict) else {}
+    trace_id = otel_id(body.get("traceId"), 16)
+    span_id = otel_id(body.get("id"), 8)
+    parent_id = body.get("parentObservationId")
+    start_time = body.get("startTime") or event.get("timestamp")
+    end_time = body.get("endTime") or start_time
+
+    attributes = {
+        "langfuse.trace.name": trace_body.get("name"),
+        "session.id": trace_body.get("sessionId"),
+        "langfuse.trace.tags": trace_body.get("tags"),
+        "langfuse.environment": body.get("environment")
+        or trace_body.get("environment"),
+        "langfuse.observation.type": (
+            "generation" if event_type == "generation-create" else "span"
+        ),
+        "langfuse.observation.input": (
+            serialized_otel_value(body["input"])
+            if body.get("input") is not None
+            else None
+        ),
+        "langfuse.observation.output": (
+            serialized_otel_value(body["output"])
+            if body.get("output") is not None
+            else None
+        ),
+        "langfuse.observation.level": body.get("level"),
+        "langfuse.observation.status_message": body.get("statusMessage"),
+    }
+    attributes.update(
+        metadata_otel_attributes(
+            "langfuse.trace.metadata", trace_body.get("metadata") or {}
+        )
+    )
+    attributes.update(
+        metadata_otel_attributes(
+            "langfuse.observation.metadata", body.get("metadata") or {}
+        )
+    )
+    if event_type == "generation-create":
+        attributes["langfuse.observation.model.name"] = body.get("model")
+        if body.get("usageDetails") is not None:
+            attributes["langfuse.observation.usage_details"] = 
serialized_otel_value(
+                body["usageDetails"]
+            )
+    if not parent_id:
+        attributes["langfuse.internal.is_app_root"] = True
+
+    span = {
+        "traceId": trace_id,
+        "spanId": span_id,
+        "name": body.get("name") or "codex.unknown",
+        "kind": 1,
+        "startTimeUnixNano": unix_nanos(start_time),
+        "endTimeUnixNano": unix_nanos(end_time),
+        "attributes": otel_attributes(attributes),
+        "status": {
+            # statusMessage is already carried by the explicit Langfuse 
attribute.
+            # Do not duplicate a potentially large failure payload here.
+            "code": 2 if body.get("level") == "ERROR" else 1,
+        },
+        "flags": 1,
+    }
+    if parent_id:
+        span["parentSpanId"] = otel_id(parent_id, 8)
+    return span
+
+
+def otlp_payload(trace_body, events):
+    spans = [legacy_event_to_otel_span(event, trace_body) for event in events]
+    return {
+        "resourceSpans": [
+            {
+                "resource": {
+                    "attributes": otel_attributes(
+                        {
+                            "service.name": "doris-code-review",
+                            "langfuse.environment": 
trace_body.get("environment"),
+                        }
+                    )
+                },
+                "scopeSpans": [
+                    {
+                        "scope": {"name": "doris-litefuse-exporter", 
"version": "2"},
+                        "spans": spans,
+                    }
+                ],
+            }
+        ]
+    }
+
+
+def otlp_span_count(payload):
+    return sum(
+        len(scope_spans.get("spans") or [])
+        for resource_spans in payload.get("resourceSpans") or []
+        for scope_spans in resource_spans.get("scopeSpans") or []
+    )
+
+
+def otlp_chunks(payload, max_payload_bytes, trace_body=None):
+    trace_body = trace_body or trace_body_from_payload(payload)
+    events = [
+        event
+        for event in payload.get("batch") or []
+        if event.get("type") in ("span-create", "generation-create")
+    ]
+    chunks = []
+
+    def add_chunk(candidate_events):
+        candidate_payload = otlp_payload(trace_body, candidate_events)
+        request_size = json_payload_bytes(candidate_payload)
+        if request_size <= max_payload_bytes:
+            chunks.append(
+                (
+                    {"batch": candidate_events},
+                    candidate_payload,
+                    request_size,
+                )
+            )
+            return
+        if len(candidate_events) > 1:
+            middle = len(candidate_events) // 2
+            add_chunk(candidate_events[:middle])
+            add_chunk(candidate_events[middle:])
+            return
+
+        event = candidate_events[0]
+        shrunk_event = shrink_event_for_payload(
+            event,
+            max_payload_bytes,
+            payload_bytes=lambda candidate: json_payload_bytes(
+                otlp_payload(trace_body, [candidate])
+            ),
+        )
+        shrunk_payload = otlp_payload(trace_body, [shrunk_event])
+        chunks.append(
+            (
+                {"batch": [shrunk_event]},
+                shrunk_payload,
+                json_payload_bytes(shrunk_payload),
+            )
+        )
+
+    if not events:
+        raise RuntimeError("Litefuse payload contains no spans for OTLP 
ingestion")
+    prechunk_limit = max(1_000, max_payload_bytes // 2)
+    # Bound multi-event OTLP encodes without truncating an individual event 
before
+    # add_chunk measures its encoded span against the full request limit.
+    for legacy_chunk, _legacy_size in chunk_payload(
+        {"batch": events}, prechunk_limit, shrink_oversized=False
+    ):
+        add_chunk(legacy_chunk["batch"])
+    return chunks
+
+
+def split_otlp_chunk(chunk, max_payload_bytes, trace_body):
+    events = chunk.get("batch") or []
+    if len(events) < 2:
+        raise RuntimeError("Cannot split an OTLP chunk with fewer than two 
spans")
+    middle = len(events) // 2
+    return otlp_chunks(
+        {"batch": events[:middle]}, max_payload_bytes, trace_body
+    ) + otlp_chunks(
+        {"batch": events[middle:]}, max_payload_bytes, trace_body
+    )
+
+
+def shrink_singleton_otlp_retry(
+    chunk, rejected_size, trace_body, attempt_count, retry_attempts
+):
+    event = (chunk.get("batch") or [])[0]
+    # Preserve a near-limit payload on the first rejection. If the server 
rejects
+    # it again, bisect the remaining reducible envelope instead of retrying one
+    # byte at a time. The last allowed retry uses the floor so a viable minimal
+    # span is attempted before the budget is exhausted.
+    if attempt_count == 1 and retry_attempts > 1:
+        return otlp_chunks(chunk, rejected_size - 1, trace_body)
+
+    def encoded_size(candidate):
+        return json_payload_bytes(otlp_payload(trace_body, [candidate]))
+
+    floor_event = shrink_event_for_payload(
+        event,
+        rejected_size,
+        payload_bytes=encoded_size,
+        minimize=True,
+    )
+    floor_size = encoded_size(floor_event)
+    if floor_size >= rejected_size:
+        raise RuntimeError(
+            "Litefuse OTLP singleton cannot be reduced below the rejected 
size: "
+            f"{rejected_size} bytes; name={(event.get('body') or 
{}).get('name')}"
+        )
+    next_limit = (
+        floor_size
+        if attempt_count == retry_attempts
+        else floor_size + (rejected_size - floor_size) // 2
+    )
+    return otlp_chunks(chunk, next_limit, trace_body)
+
+
 def post_payload_once(endpoint, public_key, secret_key, payload, 
timeout_seconds):
     auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
     request = urllib.request.Request(
@@ -1134,46 +1445,36 @@ def post_payload_once(endpoint, public_key, secret_key, 
payload, timeout_seconds
         headers={
             "Content-Type": "application/json",
             "Authorization": f"Basic {auth}",
+            "x-langfuse-ingestion-version": "4",
+            "x-langfuse-sdk-name": "doris-code-review",
+            "x-langfuse-sdk-version": "2",
         },
         method="POST",
     )
     with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
         body = response.read().decode()
         detail = json.loads(body) if body else {}
-        errors = detail.get("errors") if isinstance(detail, dict) else None
-        if errors:
-            raise RuntimeError(f"Litefuse ingestion returned errors: 
{json_attr(errors)}")
+        partial_success = (
+            detail.get("partialSuccess") or detail.get("partial_success") or {}
+            if isinstance(detail, dict)
+            else {}
+        )
+        rejected_spans = int(
+            partial_success.get("rejectedSpans")
+            or partial_success.get("rejected_spans")
+            or 0
+        )
+        if rejected_spans:
+            raise RuntimeError(
+                "Litefuse OTLP ingestion partially rejected "
+                f"{rejected_spans} spans: {json_attr(partial_success)}"
+            )
         return {
             "status": response.status,
-            "success_count": len(detail.get("successes") or [])
-            if isinstance(detail, dict)
-            else 0,
+            "success_count": otlp_span_count(payload),
         }
 
 
-def retry_payload_chunks_after_413(payload, request_size, max_payload_bytes):
-    batch = payload.get("batch") or []
-    if not batch:
-        raise RuntimeError(
-            "Litefuse ingestion returned 413 for an empty payload chunk"
-        )
-
-    next_limit = max(1_000, min(max_payload_bytes - 1, request_size // 2))
-    if len(batch) == 1:
-        event = shrink_event_for_payload(batch[0], next_limit)
-        return [({"batch": [event]}, json_payload_bytes({"batch": [event]}))]
-
-    return chunk_payload(payload, next_limit)
-
-
-def retry_payload_chunks_after_transport_error(payload, request_size, 
max_payload_bytes):
-    batch = payload.get("batch") or []
-    if len(batch) <= 1:
-        return [(payload, request_size)]
-    next_limit = max(1_000, min(max_payload_bytes - 1, request_size // 2))
-    return chunk_payload(payload, next_limit)
-
-
 def post_payload(
     endpoint,
     public_key,
@@ -1189,38 +1490,76 @@ def post_payload(
     request_sizes = []
     payload_too_large_retry_count = 0
     transport_retry_count = 0
-    chunks = chunk_payload(payload, max_payload_bytes)
+    post_attempt_count = 0
+    singleton_413_attempts = {}
+    singleton_413_sources = {}
+    trace_body = trace_body_from_payload(payload)
+    chunks = otlp_chunks(payload, max_payload_bytes, trace_body)
     while chunks:
-        chunk, request_size = chunks.pop(0)
+        chunk, otlp_chunk, request_size = chunks.pop(0)
+        post_attempt_count += 1
         try:
             status = post_payload_once(
-                endpoint, public_key, secret_key, chunk, timeout_seconds
+                endpoint, public_key, secret_key, otlp_chunk, timeout_seconds
             )
         except urllib.error.HTTPError as exc:
             if exc.code != 413:
-                raise
+                error_body = exc.read().decode("utf-8", errors="replace")
+                raise RuntimeError(
+                    "Litefuse OTLP ingestion returned "
+                    f"HTTP {exc.code}: {truncate_text(error_body, 4_000)}"
+                ) from exc
             payload_too_large_retry_count += 1
-            chunks = (
-                retry_payload_chunks_after_413(
-                    chunk, request_size, max_payload_bytes
-                )
-                + chunks
+            chunk_events = chunk.get("batch") or []
+            if len(chunk_events) > 1:
+                chunks = split_otlp_chunk(
+                    chunk, max_payload_bytes, trace_body
+                ) + chunks
+                continue
+            event = chunk_events[0]
+            body = event.get("body") if isinstance(event.get("body"), dict) 
else {}
+            event_key = (
+                event.get("type"),
+                str(body.get("traceId") or ""),
+                str(body.get("id") or ""),
             )
+            singleton_413_sources.setdefault(event_key, chunk)
+            singleton_413_attempts[event_key] = (
+                singleton_413_attempts.get(event_key, 0) + 1
+            )
+            attempt_count = singleton_413_attempts[event_key]
+            if attempt_count > retry_attempts:
+                raise RuntimeError(
+                    "Litefuse OTLP singleton remained too large after "
+                    f"{retry_attempts} retries: {request_size} bytes; "
+                    f"observation_id={body.get('id')}"
+                ) from exc
+            chunks = shrink_singleton_otlp_retry(
+                singleton_413_sources[event_key],
+                request_size,
+                trace_body,
+                attempt_count,
+                retry_attempts,
+            ) + chunks
             continue
         except (TimeoutError, urllib.error.URLError) as exc:
             transport_retry_count += 1
             if transport_retry_count > retry_attempts:
                 raise RuntimeError(
-                    "Litefuse ingestion failed after transport retries: "
+                    "Litefuse OTLP ingestion failed after transport retries: "
                     f"{type(exc).__name__}: {exc}"
                 ) from exc
             time.sleep(retry_sleep_seconds)
-            chunks = (
-                retry_payload_chunks_after_transport_error(
-                    chunk, request_size, max_payload_bytes
+            chunk_events = chunk.get("batch") or []
+            if len(chunk_events) > 1:
+                chunks = split_otlp_chunk(
+                    chunk, max_payload_bytes, trace_body
+                ) + chunks
+            else:
+                chunks.insert(
+                    0,
+                    (chunk, otlp_chunk, request_size),
                 )
-                + chunks
-            )
             continue
         statuses.append(status["status"])
         success_count += int(status.get("success_count") or 0)
@@ -1228,6 +1567,7 @@ def post_payload(
     return {
         "statuses": statuses,
         "request_count": len(statuses),
+        "post_attempt_count": post_attempt_count,
         "request_sizes": request_sizes,
         "max_request_size": max(request_sizes) if request_sizes else 0,
         "payload_too_large_retries": payload_too_large_retry_count,
@@ -1247,25 +1587,40 @@ def fetch_trace(base_url, public_key, secret_key, 
trace_id):
         return json.loads(response.read().decode())
 
 
-def fetch_observations_v2(base_url, public_key, secret_key, trace_id):
+def fetch_observations_v2(
+    base_url, public_key, secret_key, trace_id, max_pages=10
+):
     auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
     now = datetime.now(timezone.utc)
-    params = urllib.parse.urlencode(
-        {
-            "traceId": trace_id,
-            "fromStartTime": (now - 
timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
-            "toStartTime": (now + 
timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
-            "fields": "core,basic,io,trace_context,model,usage",
-            "limit": "100",
-        }
-    )
-    request = urllib.request.Request(
-        f"{base_url.rstrip('/')}/api/public/v2/observations?{params}",
-        headers={"Authorization": f"Basic {auth}"},
-        method="GET",
+    query = {
+        "traceId": trace_id,
+        "fromStartTime": (now - 
timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
+        "toStartTime": (now + 
timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
+        "fields": "core,basic,io,trace_context,model,usage",
+        "limit": "1000",
+    }
+    rows = []
+    cursor = ""
+    for _ in range(max_pages):
+        if cursor:
+            query["cursor"] = cursor
+        params = urllib.parse.urlencode(query)
+        request = urllib.request.Request(
+            f"{base_url.rstrip('/')}/api/public/v2/observations?{params}",
+            headers={"Authorization": f"Basic {auth}"},
+            method="GET",
+        )
+        with urllib.request.urlopen(request, timeout=30) as response:
+            payload = json.loads(response.read().decode())
+        rows.extend(observation_rows_from_v2(payload))
+        meta = payload.get("meta") if isinstance(payload, dict) else {}
+        cursor = meta.get("cursor") if isinstance(meta, dict) else ""
+        if not cursor:
+            return {**payload, "data": rows}
+    raise RuntimeError(
+        "Litefuse v2 observations remained paginated after "
+        f"{max_pages} pages for trace {trace_id}"
     )
-    with urllib.request.urlopen(request, timeout=30) as response:
-        return json.loads(response.read().decode())
 
 
 def fetch_observations_legacy(
@@ -1536,7 +1891,7 @@ def parse_args():
 
 def main():
     args = parse_args()
-    endpoint = args.endpoint or 
f"{args.base_url.rstrip('/')}/api/public/ingestion"
+    endpoint = args.endpoint or 
f"{args.base_url.rstrip('/')}/api/public/otel/v1/traces"
     if args.max_context_json_chars <= 0:
         args.max_context_json_chars = args.max_json_chars
 
@@ -1570,16 +1925,18 @@ def main():
     if args.dry_run:
         result["batch_count"] = len(payload["batch"])
         result["event_types"] = [event["type"] for event in 
payload["batch"][:10]]
-        chunks = chunk_payload(payload, args.max_payload_bytes)
+        chunks = otlp_chunks(payload, args.max_payload_bytes)
         result["request_count"] = len(chunks)
-        result["request_sizes"] = [request_size for _, request_size in chunks]
+        result["request_sizes"] = [request_size for _, _, request_size in 
chunks]
         result["max_request_size"] = (
             max(result["request_sizes"]) if result["request_sizes"] else 0
         )
         result["subagent_traces"] = []
         for subagent_payload in subagent_payloads:
-            chunks = chunk_payload(subagent_payload["payload"], 
args.max_payload_bytes)
-            request_sizes = [request_size for _chunk, request_size in chunks]
+            chunks = otlp_chunks(
+                subagent_payload["payload"], args.max_payload_bytes
+            )
+            request_sizes = [request_size for _chunk, _otel, request_size in 
chunks]
             result["subagent_traces"].append(
                 {
                     "trace_id": subagent_payload["trace_id"],
diff --git a/.github/scripts/test_emit_litefuse_otel_io.py 
b/.github/scripts/test_emit_litefuse_otel_io.py
new file mode 100644
index 00000000000..2b4aea839de
--- /dev/null
+++ b/.github/scripts/test_emit_litefuse_otel_io.py
@@ -0,0 +1,633 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import importlib.util
+import io
+import json
+from pathlib import Path
+from types import SimpleNamespace
+import unittest
+from unittest import mock
+import urllib.error
+import urllib.parse
+
+
+MODULE_PATH = Path(__file__).with_name("emit_litefuse_otel_io.py")
+SPEC = importlib.util.spec_from_file_location("emit_litefuse_otel_io", 
MODULE_PATH)
+MODULE = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(MODULE)
+
+
+def attribute_values(span):
+    values = {}
+    for attribute in span["attributes"]:
+        value = attribute["value"]
+        if "arrayValue" in value:
+            values[attribute["key"]] = [
+                next(iter(item.values()))
+                for item in value["arrayValue"]["values"]
+            ]
+        else:
+            values[attribute["key"]] = next(iter(value.values()))
+    return values
+
+
+class FakeResponse:
+    status = 200
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, _exc_type, _exc, _traceback):
+        return False
+
+    def read(self):
+        return b"{}"
+
+
+class PartialSuccessResponse(FakeResponse):
+    def read(self):
+        return b'{"partialSuccess":{"rejectedSpans":"1","errorMessage":"bad 
span"}}'
+
+
+class JsonResponse(FakeResponse):
+    def __init__(self, payload):
+        self.payload = payload
+
+    def read(self):
+        return json.dumps(self.payload).encode()
+
+
+class LitefuseOtelExporterTest(unittest.TestCase):
+    def trace_body(self):
+        return {
+            "id": "1" * 32,
+            "name": "doris-ai-review",
+            "sessionId": "run-123",
+            "environment": "github-actions",
+            "metadata": {"repository": "apache/doris", "codex_jsonl": True},
+            "tags": ["doris-ai-review", "codex-jsonl"],
+        }
+
+    def span_event(self, span_id, parent_id=None, output_size=0):
+        body = {
+            "id": span_id,
+            "traceId": "1" * 32,
+            "name": "codex.command",
+            "startTime": "2026-09-01T00:00:00.000Z",
+            "endTime": "2026-09-01T00:00:01.000Z",
+            "input": {"command": "git status"},
+            "output": {"status": "completed", "text": "x" * output_size},
+            "environment": "github-actions",
+            "metadata": {"item_type": "command_execution"},
+            "level": "DEFAULT",
+        }
+        if parent_id:
+            body["parentObservationId"] = parent_id
+        return {"type": "span-create", "body": body}
+
+    def reject_payloads_above(self, server_limit, request_sizes):
+        def fake_urlopen(request, timeout):
+            request_sizes.append(len(request.data))
+            if len(request.data) > server_limit:
+                raise urllib.error.HTTPError(
+                    request.full_url,
+                    413,
+                    "Payload Too Large",
+                    {},
+                    io.BytesIO(b"payload too large"),
+                )
+            return FakeResponse()
+
+        return fake_urlopen
+
+    def test_converts_legacy_events_to_otlp_hierarchy_and_attributes(self):
+        root_id = "2" * 32
+        child_id = "3" * 32
+        root = self.span_event(root_id)
+        child = self.span_event(child_id, root_id)
+
+        payload = MODULE.otlp_payload(self.trace_body(), [root, child])
+        spans = payload["resourceSpans"][0]["scopeSpans"][0]["spans"]
+
+        self.assertEqual(len(spans), 2)
+        self.assertEqual(spans[0]["traceId"], "1" * 32)
+        self.assertEqual(len(spans[0]["spanId"]), 16)
+        self.assertNotIn("parentSpanId", spans[0])
+        self.assertEqual(spans[1]["parentSpanId"], spans[0]["spanId"])
+        attributes = attribute_values(spans[0])
+        self.assertEqual(attributes["langfuse.trace.name"], "doris-ai-review")
+        self.assertEqual(attributes["session.id"], "run-123")
+        self.assertEqual(attributes["langfuse.observation.type"], "span")
+        self.assertTrue(attributes["langfuse.internal.is_app_root"])
+        self.assertEqual(
+            attributes["langfuse.trace.tags"],
+            ["doris-ai-review", "codex-jsonl"],
+        )
+
+    def test_bounds_and_shrinks_failed_turn_status_message(self):
+        args = SimpleNamespace(
+            repository="apache/doris",
+            workflow="Code Review",
+            run_id="run-123",
+            pr_number="67413",
+            head_sha="a" * 40,
+            base_sha="b" * 40,
+            reasoning_effort="xhigh",
+            max_json_chars=20_000,
+            max_context_json_chars=0,
+            trace_name="doris-ai-review",
+            session_id="run-123",
+            environment="github-actions",
+            model="gpt-5.6-sol",
+        )
+        events = [
+            {"type": "turn.failed", "error": {"message": "e" * 50_000}}
+        ]
+        _trace_id, payload, _observation_count = 
MODULE.build_ingestion_payload(
+            args, "review task", "", events
+        )
+        turn_event = next(
+            event
+            for event in payload["batch"]
+            if (event.get("body") or {}).get("name") == "codex.turn"
+        )
+        source_status = turn_event["body"]["statusMessage"]
+
+        self.assertLess(len(source_status), 20_100)
+        self.assertIn("[truncated to first 20000 chars]", source_status)
+        trace_body = MODULE.trace_body_from_payload(payload)
+        self.assertGreater(
+            MODULE.json_payload_bytes(MODULE.otlp_payload(trace_body, 
[turn_event])),
+            20_000,
+        )
+
+        chunks = MODULE.otlp_chunks(payload, max_payload_bytes=20_000)
+        spans = [
+            span
+            for _legacy, otel, _size in chunks
+            for resource in otel["resourceSpans"]
+            for scope in resource["scopeSpans"]
+            for span in scope["spans"]
+        ]
+        turn_span = next(span for span in spans if span["name"] == 
"codex.turn")
+        attributes = attribute_values(turn_span)
+
+        self.assertEqual(turn_span["status"], {"code": 2})
+        self.assertIn(
+            "[truncated to first", 
attributes["langfuse.observation.status_message"]
+        )
+        self.assertTrue(all(size <= 20_000 for _legacy, _otel, size in chunks))
+
+    def test_subagent_root_observation_preserves_task_input(self):
+        args = SimpleNamespace(
+            max_input_chars=200_000,
+            max_output_chars=200_000,
+            max_json_chars=40_000,
+            repository="apache/doris",
+            workflow="Code Review",
+            run_id="run-123",
+            pr_number="67413",
+            head_sha="a" * 40,
+            base_sha="b" * 40,
+            reasoning_effort="xhigh",
+            session_id="run-123",
+            subagent_trace_name="doris-ai-review-subagent",
+            environment="github-actions",
+            model="gpt-5.6-sol",
+        )
+        session_path = "/tmp/thread-123.jsonl"
+        events = [
+            {
+                "type": "session_meta",
+                "timestamp": "2026-09-01T00:00:00.000Z",
+                "payload": {"id": "thread-123"},
+            },
+            {
+                "type": "response_item",
+                "timestamp": "2026-09-01T00:00:01.000Z",
+                "payload": {
+                    "type": "message",
+                    "role": "user",
+                    "content": [{"type": "input_text", "text": "review task"}],
+                },
+            },
+            {
+                "type": "response_item",
+                "timestamp": "2026-09-01T00:00:02.000Z",
+                "payload": {
+                    "type": "message",
+                    "role": "assistant",
+                    "content": [{"type": "output_text", "text": "review 
result"}],
+                },
+            },
+        ]
+
+        result = MODULE.build_subagent_session_payload(args, session_path, 
events)
+        chunks = MODULE.otlp_chunks(result["payload"], 
max_payload_bytes=20_000)
+        spans = [
+            span
+            for _legacy, otel, _size in chunks
+            for resource in otel["resourceSpans"]
+            for scope in resource["scopeSpans"]
+            for span in scope["spans"]
+        ]
+        root = next(span for span in spans if span["name"] == 
"codex.subagent.review")
+        attributes = attribute_values(root)
+
+        self.assertEqual(
+            json.loads(attributes["langfuse.observation.input"]),
+            {"prompt": "review task"},
+        )
+        self.assertEqual(
+            attributes["langfuse.observation.metadata.session_file"], 
session_path
+        )
+        self.assertEqual(
+            attributes["langfuse.observation.metadata.thread_id"], "thread-123"
+        )
+
+    def test_chunks_encoded_otlp_payloads_to_requested_size(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_events = [
+            self.span_event(f"{index:032x}", output_size=2_000)
+            for index in range(1, 9)
+        ]
+
+        chunks = MODULE.otlp_chunks(
+            {"batch": [trace_event, *span_events]}, max_payload_bytes=6_000
+        )
+
+        self.assertGreater(len(chunks), 1)
+        self.assertEqual(
+            sum(MODULE.otlp_span_count(otel) for _legacy, otel, _size in 
chunks),
+            len(span_events),
+        )
+        self.assertTrue(all(size <= 6_000 for _legacy, _otel, size in chunks))
+
+    def test_truncates_one_oversized_span(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_event = self.span_event("2" * 32, output_size=50_000)
+
+        chunks = MODULE.otlp_chunks(
+            {"batch": [trace_event, span_event]}, max_payload_bytes=6_000
+        )
+
+        self.assertEqual(len(chunks), 1)
+        _legacy, otel, size = chunks[0]
+        self.assertLessEqual(size, 6_000)
+        output = attribute_values(
+            otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
+        )["langfuse.observation.output"]
+        self.assertIn("truncated_json", output)
+
+    def test_does_not_truncate_span_below_full_otlp_limit(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_event = self.span_event("2" * 32, output_size=3_500)
+        original = MODULE.otlp_payload(self.trace_body(), [span_event])
+        original_size = MODULE.json_payload_bytes(original)
+
+        self.assertGreater(original_size, 3_000)
+        self.assertLessEqual(original_size, 6_000)
+        chunks = MODULE.otlp_chunks(
+            {"batch": [trace_event, span_event]}, max_payload_bytes=6_000
+        )
+
+        self.assertEqual(len(chunks), 1)
+        _legacy, otel, size = chunks[0]
+        self.assertEqual(size, original_size)
+        output = attribute_values(
+            otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
+        )["langfuse.observation.output"]
+        self.assertNotIn("truncated_json", output)
+        self.assertEqual(len(json.loads(output)["text"]), 3_500)
+
+    def test_preserves_near_limit_single_span_payload(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_event = self.span_event("2" * 32, output_size=4_509)
+        original = MODULE.otlp_payload(self.trace_body(), [span_event])
+        original_size = MODULE.json_payload_bytes(original)
+
+        self.assertGreater(original_size, 6_000)
+        self.assertLess(original_size, 6_100)
+        chunks = MODULE.otlp_chunks(
+            {"batch": [trace_event, span_event]}, max_payload_bytes=6_000
+        )
+
+        self.assertEqual(len(chunks), 1)
+        _legacy, otel, size = chunks[0]
+        self.assertGreater(size, 5_500)
+        self.assertLessEqual(size, 6_000)
+        output = attribute_values(
+            otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
+        )["langfuse.observation.output"]
+        self.assertGreater(len(json.loads(output)["truncated_json"]), 4_000)
+
+    def test_prechunks_before_otlp_encoding(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_events = [
+            self.span_event(f"{index:032x}", output_size=1_000)
+            for index in range(1, 21)
+        ]
+        encoded_batch_sizes = []
+        original_otlp_payload = MODULE.otlp_payload
+
+        def recording_otlp_payload(trace_body, events):
+            encoded_batch_sizes.append(len(events))
+            return original_otlp_payload(trace_body, events)
+
+        with mock.patch.object(
+            MODULE, "otlp_payload", side_effect=recording_otlp_payload
+        ):
+            MODULE.otlp_chunks(
+                {"batch": [trace_event, *span_events]}, max_payload_bytes=6_000
+            )
+
+        self.assertLess(max(encoded_batch_sizes), len(span_events))
+
+    def test_posts_otlp_v4_headers(self):
+        payload = MODULE.otlp_payload(
+            self.trace_body(), [self.span_event("2" * 32)]
+        )
+        captured = {}
+
+        def fake_urlopen(request, timeout):
+            captured["request"] = request
+            captured["timeout"] = timeout
+            return FakeResponse()
+
+        with mock.patch.object(MODULE.urllib.request, "urlopen", fake_urlopen):
+            status = MODULE.post_payload_once(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                payload,
+                30,
+            )
+
+        headers = {key.lower(): value for key, value in 
captured["request"].header_items()}
+        self.assertEqual(headers["content-type"], "application/json")
+        self.assertEqual(headers["x-langfuse-ingestion-version"], "4")
+        self.assertEqual(headers["x-langfuse-sdk-name"], "doris-code-review")
+        self.assertEqual(captured["timeout"], 30)
+        self.assertEqual(status["success_count"], 1)
+
+    def test_paginates_v2_observations_until_root_is_visible(self):
+        first_page = [{"id": f"child-{index}"} for index in range(1_000)]
+        responses = [
+            JsonResponse({"data": first_page, "meta": {"cursor": "next"}}),
+            JsonResponse({"data": [{"id": "root"}], "meta": {}}),
+        ]
+        requests = []
+
+        def fake_urlopen(request, timeout):
+            requests.append((request, timeout))
+            return responses.pop(0)
+
+        with mock.patch.object(MODULE.urllib.request, "urlopen", fake_urlopen):
+            payload = MODULE.fetch_observations_v2(
+                "https://litefuse.example";, "public", "secret", "trace-id"
+            )
+
+        self.assertEqual(len(payload["data"]), 1_001)
+        self.assertEqual(payload["data"][-1], {"id": "root"})
+        first_query = urllib.parse.parse_qs(
+            urllib.parse.urlparse(requests[0][0].full_url).query
+        )
+        second_query = urllib.parse.parse_qs(
+            urllib.parse.urlparse(requests[1][0].full_url).query
+        )
+        self.assertEqual(first_query["limit"], ["1000"])
+        self.assertNotIn("cursor", first_query)
+        self.assertEqual(second_query["cursor"], ["next"])
+
+    def test_rejects_incomplete_v2_observation_pagination(self):
+        response = JsonResponse(
+            {"data": [{"id": "newest"}], "meta": {"cursor": "still-more"}}
+        )
+
+        with mock.patch.object(
+            MODULE.urllib.request, "urlopen", return_value=response
+        ):
+            with self.assertRaisesRegex(RuntimeError, "remained paginated"):
+                MODULE.fetch_observations_v2(
+                    "https://litefuse.example";,
+                    "public",
+                    "secret",
+                    "trace-id",
+                    max_pages=1,
+                )
+
+    def test_rejects_otlp_partial_success(self):
+        payload = MODULE.otlp_payload(
+            self.trace_body(), [self.span_event("2" * 32)]
+        )
+
+        with mock.patch.object(
+            MODULE.urllib.request, "urlopen", 
return_value=PartialSuccessResponse()
+        ):
+            with self.assertRaisesRegex(RuntimeError, "partially rejected 1 
spans"):
+                MODULE.post_payload_once(
+                    "https://litefuse.example/api/public/otel/v1/traces";,
+                    "public",
+                    "secret",
+                    payload,
+                    30,
+                )
+
+    def test_splits_multi_span_chunk_after_transport_error(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        payload = {
+            "batch": [
+                trace_event,
+                self.span_event("2" * 32),
+                self.span_event("3" * 32, "2" * 32),
+            ]
+        }
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=[
+                urllib.error.URLError("connection reset"),
+                FakeResponse(),
+                FakeResponse(),
+            ],
+        ):
+            status = MODULE.post_payload(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                payload,
+                10_000,
+                30,
+                3,
+                0,
+            )
+
+        self.assertEqual(status["transport_retries"], 1)
+        self.assertEqual(status["request_count"], 2)
+        self.assertEqual(status["success_count"], 2)
+
+    def test_splits_multi_span_chunk_after_http_413(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        payload = {
+            "batch": [
+                trace_event,
+                self.span_event("2" * 32),
+                self.span_event("3" * 32, "2" * 32),
+            ]
+        }
+        payload_too_large = urllib.error.HTTPError(
+            "https://litefuse.example/api/public/otel/v1/traces";,
+            413,
+            "Payload Too Large",
+            {},
+            io.BytesIO(b"payload too large"),
+        )
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=[payload_too_large, FakeResponse(), FakeResponse()],
+        ):
+            status = MODULE.post_payload(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                payload,
+                10_000,
+                30,
+                3,
+                0,
+            )
+
+        self.assertEqual(status["payload_too_large_retries"], 1)
+        self.assertEqual(status["request_count"], 2)
+        self.assertEqual(status["success_count"], 2)
+
+    def test_retries_single_span_413_without_half_size_ceiling(self):
+        trace_body = {
+            **self.trace_body(),
+            "metadata": {"repository": "apache/doris", "fixed": "m" * 3_000},
+        }
+        trace_event = {"type": "trace-create", "body": trace_body}
+        span_event = self.span_event("2" * 32, output_size=1_500)
+        server_limit = (
+            MODULE.json_payload_bytes(MODULE.otlp_payload(trace_body, 
[span_event])) - 1
+        )
+        request_sizes = []
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=self.reject_payloads_above(server_limit, 
request_sizes),
+        ):
+            status = MODULE.post_payload(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                {"batch": [trace_event, span_event]},
+                10_000,
+                30,
+                3,
+                0,
+            )
+
+        self.assertEqual(status["payload_too_large_retries"], 1)
+        self.assertEqual(status["request_count"], 1)
+        self.assertEqual(status["success_count"], 1)
+        self.assertEqual(len(request_sizes), 2)
+        self.assertLessEqual(request_sizes[1], server_limit)
+        self.assertLess(request_sizes[1], request_sizes[0])
+        self.assertGreater(request_sizes[1], request_sizes[0] // 2)
+
+    def test_adapts_single_span_413_for_lower_server_limit(self):
+        trace_body = {
+            **self.trace_body(),
+            "metadata": {"repository": "apache/doris", "fixed": "m" * 3_000},
+        }
+        trace_event = {"type": "trace-create", "body": trace_body}
+        span_event = self.span_event("2" * 32, output_size=5_000)
+        initial_size = MODULE.json_payload_bytes(
+            MODULE.otlp_payload(trace_body, [span_event])
+        )
+        server_limit = initial_size - 1_000
+        request_sizes = []
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=self.reject_payloads_above(server_limit, 
request_sizes),
+        ):
+            status = MODULE.post_payload(
+                "https://litefuse.example/api/public/otel/v1/traces";,
+                "public",
+                "secret",
+                {"batch": [trace_event, span_event]},
+                10_000,
+                30,
+                5,
+                0,
+            )
+
+        self.assertEqual(initial_size, 9_486)
+        self.assertEqual(len(request_sizes), 3)
+        self.assertTrue(
+            all(
+                current > following
+                for current, following in zip(request_sizes, request_sizes[1:])
+            )
+        )
+        self.assertLessEqual(request_sizes[-1], server_limit)
+        self.assertEqual(status["payload_too_large_retries"], 2)
+        self.assertEqual(status["post_attempt_count"], len(request_sizes))
+        self.assertEqual(status["success_count"], 1)
+
+    def test_stops_single_span_413_after_retry_budget(self):
+        trace_event = {"type": "trace-create", "body": self.trace_body()}
+        span_event = self.span_event("2" * 32, output_size=5_000)
+        request_sizes = []
+
+        with mock.patch.object(
+            MODULE.urllib.request,
+            "urlopen",
+            side_effect=self.reject_payloads_above(-1, request_sizes),
+        ):
+            with self.assertRaisesRegex(RuntimeError, "after 3 retries"):
+                MODULE.post_payload(
+                    "https://litefuse.example/api/public/otel/v1/traces";,
+                    "public",
+                    "secret",
+                    {"batch": [trace_event, span_event]},
+                    10_000,
+                    30,
+                    3,
+                    0,
+                )
+
+        self.assertEqual(len(request_sizes), 4)
+        self.assertTrue(
+            all(
+                current > following
+                for current, following in zip(request_sizes, request_sizes[1:])
+            )
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to