litellm/tests/e2e/logging/s3_reader.py
yucheng-berri f0fadb7f99
test(e2e): add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure) (#38552)
* test: add logging e2e coverage (s3_v2, gcs_bucket, team langfuse callback, datadog failure)

Five new live e2e scenarios raising Logging & Guardrails registry coverage:
s3_v2 success and failure objects read back from the real S3 bucket,
gcs_bucket success record read back through the GCS JSON API (with
nextPageToken pagination and per-request bearer minting), team-scoped
Langfuse callback delivery with non-team isolation, and DataDog failure
event delivery queried by indexed model_group. datadog_reader gains
query-based variants of the marker search; the langfuse cell is a new
registry row. Bucket readers settle past a full flush interval so a
late duplicate cannot hide from the exactly-one assertions

* test: cover clock-skew day prefix in gcs read-back and retry team callback propagation

* test: key the s3 failure read-back on the provider error, not payload absence

* chore: rerun ci

* chore: rerun ci after config sync

* chore: rerun ci with pr lane env

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* chore: rerun ci

* test: add guardrail e2e coverage (presidio masking, bedrock post and during call, moderation on messages) (#38553)

* test: add guardrail e2e coverage (presidio masking, bedrock post/during, moderation on messages)

* test: require the phone placeholder positively in the presidio masking predicate

* test: count only the 400 verdict body as a bedrock post_call block

* test(e2e): exempt the guardrail config echo from the post_call leak assertion

* test(e2e): pin the fail-closed contract for an unknown guardrail name (skipped, product gap)

* test(e2e): tolerate the readiness 503 from a transient db blip in the callback-config probes
2026-08-29 09:43:44 -07:00

115 lines
5 KiB
Python

"""Read-back for the s3 logging tests against the real S3 bucket the proxy
ships StandardLoggingPayload objects to (litellm_settings.callbacks: ["s3_v2"]).
Delivery is judged on what actually landed in the bucket: the proxy writes
with its own credentials exactly as in production, and the tests list and
download the objects back with boto3 (already a litellm proxy dependency, so
the e2e runner image carries it; it is an AWS SDK, not a raw HTTP client, so
the e2e_http-only transport rule is untouched). The bucket comes from
S3_LOGS_BUCKET_NAME - on the cluster the secret manager injects it, locally
tests/e2e/.env provides it. Missing configuration is a hard failure, never a
skip.
"""
from __future__ import annotations
import os
import time
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
import boto3
import pytest
from pydantic import BaseModel, ConfigDict
from e2e_config import POLL_INTERVAL, POLL_TIMEOUT
if TYPE_CHECKING:
from types_boto3_s3.client import S3Client
#: How long to keep re-reading after the first match before trusting the
#: exactly-one assertion: past one full s3_v2 flush interval (~10s), so a
#: duplicate shipped by a LATER flush is seen, plus listing-latency margin.
#: The DataDog reader settles the same way (DD_SETTLE_SECONDS).
S3_SETTLE_SECONDS = 25.0
class S3LogRecord(BaseModel):
"""The StandardLoggingPayload fields the s3 scenarios pin."""
model_config = ConfigDict(extra="ignore")
id: str
status: str
model_group: str | None = None
response_cost: float | None = None
total_tokens: int | None = None
error_str: str | None = None
@dataclass(frozen=True, slots=True)
class S3LogReader:
bucket: str
client: S3Client
def list_keys(self, prefix: str) -> list[str]:
response = self.client.list_objects_v2(Bucket=self.bucket, Prefix=prefix)
return [obj["Key"] for obj in response.get("Contents", []) if "Key" in obj]
def read_record(self, key: str) -> S3LogRecord:
body = self.client.get_object(Bucket=self.bucket, Key=key)["Body"].read()
return S3LogRecord.model_validate_json(body)
def records_matching(self, *, prefix: str, predicate: Callable[[S3LogRecord], bool]) -> list[S3LogRecord]:
return [record for record in map(self.read_record, self.list_keys(prefix)) if predicate(record)]
def poll_records(self, *, prefix: str, predicate: Callable[[S3LogRecord], bool]) -> list[S3LogRecord]:
"""Poll until at least one matching object is listed (the s3_v2
callback flushes on a ~10s timer), then keep re-reading for
S3_SETTLE_SECONDS - past a full flush interval - so a duplicate
shipped by a later flush cannot hide from the exactly-one assertion.
One blind spot is inherent: a duplicate write that reuses the exact
same object key overwrites the first object and no listing can see
it; distinct-key duplicates are what this catches. At the deadline an
empty list is returned and the caller's assertion carries the failure
message."""
deadline = time.monotonic() + POLL_TIMEOUT
while time.monotonic() < deadline:
records = self.records_matching(prefix=prefix, predicate=predicate)
if records:
return self._settled_records(prefix=prefix, predicate=predicate, first=records)
time.sleep(POLL_INTERVAL)
return []
def _settled_records(
self, *, prefix: str, predicate: Callable[[S3LogRecord], bool], first: list[S3LogRecord]
) -> list[S3LogRecord]:
"""Re-read at every poll interval until the settle window closes; a
duplicate ends the watch early because more waiting cannot clear it.
A transiently empty re-read never downgrades what was already seen."""
settle_deadline = time.monotonic() + S3_SETTLE_SECONDS
latest = first
while time.monotonic() < settle_deadline and len(latest) <= 1:
time.sleep(POLL_INTERVAL)
latest = self.records_matching(prefix=prefix, predicate=predicate) or latest
return latest
def build_s3_reader() -> S3LogReader:
bucket = os.environ.get("S3_LOGS_BUCKET_NAME", "")
if not bucket:
pytest.fail(
"S3_LOGS_BUCKET_NAME must be set: the s3 tests read the proxy's s3_v2 "
"delivery back from the real bucket (the cluster secret manager injects "
"it; locally set it in tests/e2e/.env to the same bucket "
"s3_callback_params.s3_bucket_name names)"
)
region = os.environ.get("AWS_REGION_NAME") or os.environ.get("AWS_REGION") or "us-east-1"
return S3LogReader(
bucket=bucket,
# boto3.client's overload set covers every AWS service; the ones without
# installed stubs type as Unknown, so the member is "partially unknown"
# even though the s3 overload itself resolves to S3Client.
client=boto3.client("s3", region_name=region), # pyright: ignore[reportUnknownMemberType]
)