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
12 changes: 9 additions & 3 deletions api/oss/src/apis/fastapi/otlp/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from typing import List, Optional
from datetime import datetime

from pydantic import BaseModel, ConfigDict

from oss.src.utils.exceptions import Support
from pydantic import BaseModel, ConfigDict, Field

from oss.src.core.otel.dtos import (
OTelSpanDTO,
Expand All @@ -15,7 +14,14 @@


class CollectStatusResponse(Support):
status: str
"""OTLP endpoint readiness response."""

status: str = Field(
description=(
"Readiness string. `ready` means the router is mounted and "
"accepts OTLP ingest."
),
)


class OTelTracingResponse(Support):
Expand Down
35 changes: 35 additions & 0 deletions api/oss/src/apis/fastapi/otlp/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,48 @@ def __init__(

@intercept_exceptions()
async def otlp_status(self):
"""Return the OTLP endpoint liveness status.

Lightweight readiness probe. Returns `{"status": "ready"}` when
the router is mounted. Intended for health checks from OTel
collectors before they start exporting traces.
"""
return CollectStatusResponse(status="ready")

@intercept_exceptions()
async def otlp_ingest(
self,
request: Request,
):
"""Ingest traces via the OTLP/HTTP protobuf protocol.

This endpoint accepts a serialized
`ExportTraceServiceRequest` protobuf. Point any OTLP/HTTP
collector or SDK at `POST /otlp/v1/traces` and spans will flow
into the same ingest stream as the Agenta-native endpoints.

Use this when you already have OTel instrumentation emitting
OTLP. For new integrations that don't need raw OTLP, prefer
`POST /tracing/spans/ingest` — it takes JSON, accepts Agenta's
nested shape directly, and surfaces parse failures immediately.

## Content-Type and size limit

Binary protobuf only (`Content-Type: application/x-protobuf`).
JSON OTLP is not accepted. Requests larger than the configured
batch limit (default 4 MB, see `OTLP_MAX_BATCH_BYTES`) return
`413 Request Entity Too Large`.

## Response

Successful ingest returns `200 OK` with a serialized
`ExportTraceServiceResponse` protobuf. Parse failures on the
request body return `400`; malformed spans return `500`; quota
exhaustion returns `403`. Like the native ingest paths, spans
are queued on a Redis stream and persisted asynchronously — see
[Tracing — Async write
contract](/reference/api-guide/tracing#async-write-contract-202).
"""
# -------------------------------------------------------------------- #
# Permission check
# -------------------------------------------------------------------- #
Expand Down
90 changes: 77 additions & 13 deletions api/oss/src/apis/fastapi/traces/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Optional, List

from pydantic import BaseModel

from pydantic import BaseModel, Field
from oss.src.utils.exceptions import Support

from oss.src.core.shared.dtos import Link, Windowing
Expand All @@ -14,31 +13,96 @@


class SimpleTraceCreateRequest(BaseModel):
trace: SimpleTraceCreate
"""Request body for creating a single-span "simple" trace."""

trace: SimpleTraceCreate = Field(
description=(
"The trace to create. Must include `data` (the payload being "
"recorded) and typically `origin`, `kind`, and `channel` to "
"describe where it came from. Optional `references` link the "
"trace to Agenta entities (app, variant, revision, evaluator, "
"testset, etc.)."
),
)


class SimpleTraceEditRequest(BaseModel):
trace: SimpleTraceEdit
"""Request body for editing an existing "simple" trace."""

trace: SimpleTraceEdit = Field(
description=(
"The fields to update. `data` is required. `tags`, `meta`, "
"`references`, and `links` overwrite their current values "
"when present."
),
)


class SimpleTraceQueryRequest(BaseModel):
trace: Optional[SimpleTraceQuery] = None
"""Request body for `POST /simple/traces/query`."""

trace: Optional[SimpleTraceQuery] = Field(
default=None,
description=(
"Filter fields on the trace itself — `origin`, `kind`, "
"`channel`, `tags`, `meta`, `references`, and inbound `links`. "
"Filtering by `trace.links.invocation` is the common pattern "
"for finding annotations on a given span."
),
)
#
links: Optional[List[Link]] = None
links: Optional[List[Link]] = Field(
default=None,
description=(
"Batch GET by the trace's own `(trace_id, span_id)`. Each "
"entry matches the trace whose own identity equals the pair. "
"Distinct from `trace.links`, which filters on inbound links."
),
)
#
windowing: Optional[Windowing] = None
windowing: Optional[Windowing] = Field(
default=None,
description="Cursor pagination and time range.",
)


class SimpleTraceResponse(Support):
count: int = 0
trace: Optional[SimpleTrace] = None
"""Response from a single-trace create/fetch/edit."""

count: int = Field(
default=0,
description="`1` if the trace was returned, `0` otherwise.",
)
trace: Optional[SimpleTrace] = Field(
default=None,
description=(
"The created or fetched trace, including server-assigned "
"`trace_id` and `span_id`."
),
)


class SimpleTracesResponse(Support):
count: int = 0
traces: List[SimpleTrace] = []
"""Response from `POST /simple/traces/query`."""

count: int = Field(
default=0,
description="Number of matching traces in this page.",
)
traces: List[SimpleTrace] = Field(
default=[],
description="The matching traces in the high-level `SimpleTrace` shape.",
)


class SimpleTraceLinkResponse(Support):
count: int = 0
link: Optional[Link] = None
"""Response from `DELETE /simple/traces/{trace_id}`."""

count: int = Field(
default=0,
description="`1` if a trace was removed, `0` otherwise.",
)
link: Optional[Link] = Field(
default=None,
description="The `(trace_id, span_id)` pair that was removed.",
)
81 changes: 81 additions & 0 deletions api/oss/src/apis/fastapi/traces/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ async def create_trace(
*,
trace_create_request: SimpleTraceCreateRequest,
) -> SimpleTraceResponse:
"""Create a single-span "simple" trace.

This endpoint is a higher-level helper for the common case of
recording one self-contained event — an evaluator output, a human
annotation, a feedback entry, a manually-logged inference. It
creates one span under a fresh `trace_id` and returns the resulting
handle.

## When to use this vs. `/tracing/spans/ingest`

- **Use this endpoint** when you have a single payload to record
with no internal hierarchy: evaluation results, human feedback,
manual annotations, or a standalone completion. It takes care of
`trace_id`/`span_id` generation, attribute namespacing, and link
wiring for you.
- **Use `POST /tracing/spans/ingest`** when you need multi-span
traces (e.g. an agent run with nested tool calls and LLM spans),
precise control over IDs, timings, or parent/child relationships,
or when forwarding traces from another OTel-compatible source.

## Request body

Send a `trace` object with:

- `origin` — who produced the trace (`human`, `auto`, `custom`).
- `kind` — intent (`adhoc`, `eval`, `play`).
- `channel` — transport that produced it (`sdk`, `api`, `web`, `otlp`).
- `data` — required dict carrying the actual payload (inputs,
outputs, or evaluator results).
- `tags`, `meta` — optional free-form dicts for filtering and
metadata.
- `references` — optional links to Agenta entities (application,
variant, revision, evaluator, testset, etc.).
- `links` — optional OTel-style links to other traces/spans.

Use `PATCH /preview/tracing/traces/{trace_id}` to update fields
later, `GET` to fetch, and `DELETE` to remove. See
[Tracing — References and links](/reference/api-guide/tracing#references-and-entity-linking)
for when to use `references` vs. `links`.
"""
if is_ee():
if not await check_action_access( # type: ignore
user_uid=request.state.user_id,
Expand Down Expand Up @@ -122,6 +162,14 @@ async def fetch_trace(
*,
trace_id: str,
) -> Union[Response, SimpleTraceResponse]:
"""Fetch a single "simple" trace by `trace_id`.

Returns the high-level `SimpleTrace` view (origin, kind, channel,
data, references, links) rather than the raw OTel span shape. Use
this for evaluation results, feedback entries, and annotations
created via `POST /simple/traces/`. For the span-level view of the
same trace, call `GET /tracing/traces/{trace_id}`.
"""
if is_ee():
if not await check_action_access( # type: ignore
user_uid=request.state.user_id,
Expand Down Expand Up @@ -149,6 +197,17 @@ async def edit_trace(
#
trace_edit_request: SimpleTraceEditRequest,
) -> SimpleTraceResponse:
"""Update an existing "simple" trace.

Supplied fields overwrite the existing trace. Fields not present
in the request body are left unchanged. `data` is required (the
payload being recorded); `tags`, `meta`, `references`, and
`links` are optional.

This endpoint is intended for annotations and feedback entries,
where the `data.outputs` is the part that typically gets revised.
For span-level edits, use `PUT /tracing/traces/{trace_id}`.
"""
if is_ee():
if not await check_action_access( # type: ignore
user_uid=request.state.user_id,
Expand Down Expand Up @@ -179,6 +238,14 @@ async def delete_trace(
*,
trace_id: str,
) -> SimpleTraceLinkResponse:
"""Delete a "simple" trace.

Removes the single-span trace created via
`POST /simple/traces/`. Returns the `(trace_id, span_id)` pair
that was removed, for logging or downstream cleanup. Use
`DELETE /tracing/traces/{trace_id}` when operating on a
multi-span trace.
"""
if is_ee():
if not await check_action_access( # type: ignore
user_uid=request.state.user_id,
Expand Down Expand Up @@ -206,6 +273,20 @@ async def query_traces(
*,
trace_query_request: SimpleTraceQueryRequest,
) -> SimpleTracesResponse:
"""Query "simple" traces.

Filter annotations and feedback by `origin`, `kind`, `channel`,
`tags`, `meta`, `references`, and `links`. The shape of the
request body is described in the
[Simple Endpoints](/reference/api-guide/simple-endpoints#query-traces)
guide, including the distinction between filtering via
`trace.links` (inbound links on the trace) and the top-level
`links` (batch GET by the trace's own IDs).

Use this endpoint when building feedback or annotation UIs.
For span-level queries across all trace types, use
`POST /tracing/spans/query`.
"""
if is_ee():
if not await check_action_access( # type: ignore
user_uid=request.state.user_id,
Expand Down
Loading
Loading