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
77 changes: 66 additions & 11 deletions src/sentry/api/endpoints/organization_trace_logs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, TypedDict
from typing import Annotated, NotRequired, TypedDict

from django.http import HttpResponse
from drf_spectacular.utils import OpenApiParameter, extend_schema
Expand All @@ -18,32 +18,87 @@
RESPONSE_NOT_FOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.descriptions import ApiDescription
from sentry.apidocs.examples.trace_item_attribute_examples import TraceItemAttributeExamples
from sentry.apidocs.formats import IsoDateTime
from sentry.apidocs.parameters import CursorQueryParam, GlobalParams
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.organizations.services.organization import RpcOrganization
from sentry.search.eap import constants
from sentry.search.eap.types import SearchResolverConfig
from sentry.search.eap.types import Confidence, SearchResolverConfig
from sentry.search.events.types import EventsResponse, SnubaParams
from sentry.snuba.ourlogs import OurLogs
from sentry.snuba.referrer import Referrer
from sentry.utils.validators import INVALID_ID_DETAILS, is_event_id


class TraceLogsMeta(TypedDict, total=False):
fields: dict[str, str]
full_scan: bool
bytes_scanned: int | None
routingHint: str
class TraceLogsMeta(TypedDict):
fields: Annotated[
dict[str, str], ApiDescription("Type of each returned field, keyed by field name")
]
full_scan: NotRequired[
Annotated[
bool,
ApiDescription(
"Whether the query scanned the full data set rather than a downsampled sample"
),
]
]
bytes_scanned: NotRequired[
Annotated[
int | None,
ApiDescription("Number of bytes the query scanned, or null if it wasn't reported"),
]
]
routingHint: NotRequired[
Annotated[
str,
ApiDescription(
"Opaque routing hint of the query. Pass it unchanged as routing_hint when "
"fetching the details of a log"
),
]
]


# Functional syntax because `project.id` isn't a valid identifier.
TraceLog = TypedDict(
"TraceLog",
{
"id": Annotated[str, ApiDescription("The ID of the log")],
"project.id": Annotated[int, ApiDescription("The ID of the project the log belongs to")],
"trace": Annotated[str, ApiDescription("The ID of the trace the log belongs to")],
"severity_number": Annotated[
int,
ApiDescription(
"The OpenTelemetry severity number of the log, from 1 to 24, or 0 if unset"
),
],
"severity": Annotated[
str, ApiDescription("The severity text of the log, such as `info` or `error`")
],
"timestamp": Annotated[IsoDateTime, ApiDescription("When the log was recorded")],
"timestamp_precise": Annotated[
int, ApiDescription("When the log was recorded, in nanoseconds since the Unix epoch")
],
"message": Annotated[str, ApiDescription("The body of the log")],
},
)


# Only used for api docs
class TraceLogsResponse(TypedDict):
data: list[dict[str, Any]]
meta: TraceLogsMeta
confidence: list[dict[str, Any]]
data: Annotated[list[TraceLog], ApiDescription("The logs, sorted by the `sort` parameter")]
meta: Annotated[TraceLogsMeta, ApiDescription("Metadata about the query and its fields")]
confidence: Annotated[
list[dict[str, Confidence]],
ApiDescription(
"Sampling confidence of each row in `data`, keyed by field. Logs are not "
"aggregated, so each entry is an empty object"
),
]


# The only columns trace logs can be sorted by; each may be prefixed with `-`.
Expand All @@ -65,7 +120,7 @@ class OrganizationTraceLogsEndpoint(OrganizationEventsEndpointBase):
"""Replaces a call to events that isn't possible for team plans because of projects restrictions"""

publish_status = {
"GET": ApiPublishStatus.PRIVATE,
"GET": ApiPublishStatus.PUBLIC,
}

def get_projects(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"severity_number": 9,
"severity": "INFO",
"timestamp": "2026-09-01T12:00:00+00:00",
"timestamp_precise": 1.756728000123e18,
"timestamp_precise": 1756728000123000000,
"message": "Payment processed",
}
],
Expand Down
31 changes: 31 additions & 0 deletions tests/apidocs/endpoints/events/test_organization_trace_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.test.client import RequestFactory
from django.urls import reverse

from fixtures.apidocs_test_case import APIDocsTestCase
from sentry.testutils.cases import OurLogTestCase, SnubaTestCase
from sentry.testutils.helpers.datetime import before_now


class OrganizationTraceLogsDocs(APIDocsTestCase, SnubaTestCase, OurLogTestCase):
def setUp(self) -> None:
self.login_as(user=self.user)
self.trace_id = "a" * 32
self.store_eap_items(
[
self.create_ourlog(
{"body": "Payment processed", "trace_id": self.trace_id},
timestamp=before_now(minutes=10),
),
]
)
self.url = reverse(
"sentry-api-0-organization-trace-logs",
kwargs={"organization_id_or_slug": self.organization.slug},
)

def test_get(self) -> None:
response = self.client.get(self.url, {"traceId": self.trace_id, "statsPeriod": "1h"})
assert len(response.data["data"]) == 1
request = RequestFactory().get(self.url)

self.validate_schema(request, response)
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@ def _query_params(self, endpoint: PublicGetEndpoint | PublicMutationEndpoint) ->
return {"service_span": "permission-matrix-span", "project_id": self.project.id}
if endpoint.endpoint_name == "OrganizationTraceItemAttributesEndpoint":
return {"dataset": "spans", "project": [self.project.id]}
if endpoint.endpoint_name == "OrganizationTraceLogsEndpoint":
return {"traceId": uuid4().hex, "statsPeriod": "1h"}
if endpoint.endpoint_name == "OrganizationTraceItemStatsEndpoint":
return {
"statsType": ["attributeDistributions"],
Expand Down
Loading