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
10 changes: 10 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ V1 scope:

- `projects` table for organization-scoped project discovery
- `issues` table for project-scoped operational issue inspection
- `logs` table for Explore-backed log inspection
- includes curated columns plus `extra_json` for raw additional event context
- `logs_timeseries` table for Explore-backed log volume over time
- read-only `SELECT` support

Internal organization:

- `issue/` owns the current `projects` and `issues` flow
- `explore/` owns the Explore-backed `logs` and `logs_timeseries` flow
- `sentry_client.py` and `connection_args.py` stay at the package root as shared/common pieces
- `sentry_handler.py` remains the public compatibility entrypoint

Example connection:

```sql
Expand Down
26 changes: 26 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/explore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from .client import ExploreClient
from .errors import (
ExploreAuthenticationError,
ExploreCapabilityError,
ExploreError,
ExplorePermissionError,
ExploreQueryError,
)
from .handler import ExploreSentryHandler
from .models import ExploreDataset, ExploreTableRequest, ExploreTimeseriesRequest
from .tables import SentryLogsTable, SentryLogsTimeseriesTable

__all__ = [
"ExploreClient",
"ExploreError",
"ExploreAuthenticationError",
"ExplorePermissionError",
"ExploreCapabilityError",
"ExploreQueryError",
"ExploreDataset",
"ExploreTableRequest",
"ExploreTimeseriesRequest",
"ExploreSentryHandler",
"SentryLogsTable",
"SentryLogsTimeseriesTable",
]
122 changes: 122 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/explore/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from __future__ import annotations

from typing import Any

from mindsdb.integrations.handlers.sentry_handler.explore.errors import (
ExploreAuthenticationError,
ExploreCapabilityError,
ExplorePermissionError,
ExploreQueryError,
)
from mindsdb.integrations.handlers.sentry_handler.explore.models import (
ExploreTableRequest,
ExploreTimeseriesRequest,
)
from mindsdb.integrations.handlers.sentry_handler.sentry_client import SentryClient, SentryRequestError


class ExploreClient:
def __init__(self, *, sentry_client: SentryClient, environment: str | None = None) -> None:
self.sentry_client = sentry_client
self.environment = environment

def query_table(self, request: ExploreTableRequest) -> list[dict[str, Any]]:
payload, _ = self._request(
"/events/",
params=self._build_table_params(request),
operation=f"explore {request.dataset.value} table",
)
if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
raise ExploreQueryError("Sentry explore table request returned malformed payload")
return payload["data"]

def query_timeseries(self, request: ExploreTimeseriesRequest) -> list[dict[str, Any]]:
payload, _ = self._request(
"/events-timeseries/",
params=self._build_timeseries_params(request),
operation=f"explore {request.dataset.value} timeseries",
)
if not isinstance(payload, dict) or not isinstance(payload.get("timeSeries"), list):
raise ExploreQueryError("Sentry explore timeseries request returned malformed payload")
return payload["timeSeries"]

def _request(
self,
path: str,
*,
params: dict[str, Any],
operation: str,
) -> tuple[Any, Any]:
try:
return self.sentry_client.request_json(
"GET",
f"/organizations/{self.sentry_client.organization_slug}{path}",
params=params,
operation=operation,
)
except SentryRequestError as exc:
if exc.status_code == 401:
raise ExploreAuthenticationError(str(exc)) from exc
if exc.status_code == 403:
raise ExplorePermissionError(str(exc)) from exc
if exc.status_code == 404:
raise ExploreCapabilityError(str(exc)) from exc
raise ExploreQueryError(str(exc)) from exc

def _build_table_params(self, request: ExploreTableRequest) -> dict[str, Any]:
params = self._build_common_params(
dataset=request.dataset.value,
project_ids=request.project_ids,
environments=request.environments,
start=request.start,
end=request.end,
stats_period=request.stats_period,
query=request.query,
)
params["field"] = request.fields
params["per_page"] = request.limit
if request.sort:
params["sort"] = request.sort
return params

def _build_timeseries_params(self, request: ExploreTimeseriesRequest) -> dict[str, Any]:
params = self._build_common_params(
dataset=request.dataset.value,
project_ids=request.project_ids,
environments=request.environments,
start=request.start,
end=request.end,
stats_period=request.stats_period,
query=request.query,
)
params["yAxis"] = request.y_axis
params["interval"] = request.interval
return params

@staticmethod
def _build_common_params(
*,
dataset: str,
project_ids: list[int],
environments: list[str],
start: str | None,
end: str | None,
stats_period: str | None,
query: str,
) -> dict[str, Any]:
params: dict[str, Any] = {
"dataset": dataset,
"project": project_ids,
}
if environments:
params["environment"] = environments
if query:
params["query"] = query
if stats_period:
params["statsPeriod"] = stats_period
else:
if start:
params["start"] = start
if end:
params["end"] = end
return params
18 changes: 18 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/explore/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class ExploreError(RuntimeError):
"""Base exception for Sentry Explore failures."""


class ExploreAuthenticationError(ExploreError):
"""Raised when the Explore request is rejected due to authentication."""


class ExplorePermissionError(ExploreError):
"""Raised when the Explore request lacks the required permissions."""


class ExploreCapabilityError(ExploreError):
"""Raised when the Explore dataset or endpoint is unavailable."""


class ExploreQueryError(ExploreError):
"""Raised when an Explore request or payload is invalid."""
44 changes: 44 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/explore/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

from typing import Any

from mindsdb.integrations.handlers.sentry_handler.explore.client import ExploreClient
from mindsdb.integrations.handlers.sentry_handler.explore.tables import (
SentryLogsTable,
SentryLogsTimeseriesTable,
)
from mindsdb.integrations.libs.api_handler import APIHandler


class ExploreSentryHandler(APIHandler):
name = "sentry_explore"

def __init__(
self,
name: str,
connection_data: dict[str, Any],
*,
issue_handler: Any,
) -> None:
super().__init__(name)
self.connection_data = connection_data or {}
self.issue_handler = issue_handler
self.environment = self.connection_data["environment"]
self.connection: ExploreClient | None = None
self.is_connected = False
self.thread_safe = True

self._register_table("logs", SentryLogsTable(self))
self._register_table("logs_timeseries", SentryLogsTimeseriesTable(self))

def connect(self) -> ExploreClient:
if self.is_connected and self.connection is not None:
return self.connection

sentry_client = self.issue_handler.connect()
self.connection = ExploreClient(
sentry_client=sentry_client,
environment=self.environment,
)
self.is_connected = True
return self.connection
35 changes: 35 additions & 0 deletions mindsdb/integrations/handlers/sentry_handler/explore/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum


class ExploreDataset(str, Enum):
LOGS = "logs"


@dataclass(frozen=True)
class ExploreTableRequest:
dataset: ExploreDataset
fields: list[str]
query: str
limit: int
sort: str | None
start: str | None
end: str | None
stats_period: str | None
project_ids: list[int]
environments: list[str]


@dataclass(frozen=True)
class ExploreTimeseriesRequest:
dataset: ExploreDataset
query: str
y_axis: str
interval: int
start: str | None
end: str | None
stats_period: str | None
project_ids: list[int]
environments: list[str]
Loading
Loading