Skip to content

Commit a244219

Browse files
committed
♻️ more directly copy plugins
1 parent 8424cd1 commit a244219

File tree

6 files changed

+136
-145
lines changed

6 files changed

+136
-145
lines changed

src/sentry/integrations/data_forwarding/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from collections.abc import Mapping
2+
13
from sentry.integrations.data_forwarding.amazon_sqs.forwarder import AmazonSQSForwarder
4+
from sentry.integrations.data_forwarding.base import BaseDataForwarder
25
from sentry.integrations.data_forwarding.segment.forwarder import SegmentForwarder
36
from sentry.integrations.data_forwarding.splunk.forwarder import SplunkForwarder
47
from sentry.integrations.types import DataForwarderProviderSlug
58

6-
FORWARDER_REGISTRY = {
9+
FORWARDER_REGISTRY: Mapping[DataForwarderProviderSlug, type[BaseDataForwarder]] = {
710
DataForwarderProviderSlug.SEGMENT.value: SegmentForwarder,
811
DataForwarderProviderSlug.SQS.value: AmazonSQSForwarder,
912
DataForwarderProviderSlug.SPLUNK.value: SplunkForwarder,

src/sentry/integrations/data_forwarding/amazon_sqs/forwarder.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from botocore.client import Config
1010
from botocore.exceptions import ClientError, ParamValidationError
1111

12+
from sentry.api.serializers import serialize
1213
from sentry.integrations.data_forwarding.base import BaseDataForwarder
13-
from sentry.integrations.models.data_forwarder_project import DataForwarderProject
1414
from sentry.integrations.types import DataForwarderProviderSlug
1515
from sentry.services.eventstore.models import Event
1616

@@ -19,26 +19,15 @@
1919
# AWS SQS maximum message size limit
2020
AWS_SQS_MAX_MESSAGE_SIZE = 256 * 1024 # 256 KB
2121

22-
DESCRIPTION = """
23-
Send Sentry events to Amazon SQS.
24-
25-
This integration allows you to forward Sentry events to an Amazon SQS queue for further processing.
26-
27-
Amazon SQS is a fully managed message queuing service that enables you to decouple and scale microservices.
28-
"""
29-
3022

3123
class AmazonSQSForwarder(BaseDataForwarder):
3224
provider = DataForwarderProviderSlug.SQS
33-
rate_limit = None
34-
description = DESCRIPTION
25+
rate_limit = (0, 0)
3526

36-
@classmethod
37-
def get_event_payload(cls, event: Event) -> dict[str, Any]:
38-
return event.as_dict()
27+
def get_event_payload(self, event: Event, config: dict[str, Any]) -> dict[str, Any]:
28+
return serialize(event)
3929

40-
@classmethod
41-
def is_unrecoverable_client_error(cls, error: ClientError) -> bool:
30+
def is_unrecoverable_client_error(self, error: ClientError) -> bool:
4231
error_str = str(error)
4332

4433
# Invalid or missing AWS credentials
@@ -70,9 +59,8 @@ def is_unrecoverable_client_error(cls, error: ClientError) -> bool:
7059

7160
return False
7261

73-
@classmethod
74-
def send_payload(
75-
cls,
62+
def forward_event(
63+
self,
7664
payload: dict[str, Any],
7765
config: dict[str, Any],
7866
event: Event,
@@ -125,7 +113,7 @@ def sqs_send_message(message):
125113
payload = {"s3Url": url, "eventID": event.event_id}
126114

127115
except ClientError as e:
128-
if cls.is_unrecoverable_client_error(e):
116+
if self.is_unrecoverable_client_error(e):
129117
return False
130118
raise
131119
except ParamValidationError:
@@ -141,14 +129,8 @@ def sqs_send_message(message):
141129
try:
142130
sqs_send_message(message)
143131
except ClientError as e:
144-
if cls.is_unrecoverable_client_error(e):
132+
if self.is_unrecoverable_client_error(e):
145133
return False
146134
raise
147135

148136
return True
149-
150-
@classmethod
151-
def forward_event(cls, event: Event, data_forwarder_project: DataForwarderProject) -> bool:
152-
config = data_forwarder_project.get_config()
153-
payload = cls.get_event_payload(event)
154-
return cls.send_payload(payload, config, event)
Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
1-
from __future__ import annotations
2-
1+
import logging
32
from abc import ABC, abstractmethod
4-
from typing import ClassVar
3+
from typing import Any, ClassVar
54

5+
from sentry import ratelimits
66
from sentry.integrations.models.data_forwarder_project import DataForwarderProject
77
from sentry.integrations.types import DataForwarderProviderSlug
88
from sentry.services.eventstore.models import Event
99

10+
logger = logging.getLogger(__name__)
11+
1012

1113
class BaseDataForwarder(ABC):
12-
"""Base class for all data forwarders."""
14+
"""
15+
Base class for all data forwarders.
16+
Copied directly from legacy plugin system, unchanged.
17+
"""
1318

1419
provider: ClassVar[DataForwarderProviderSlug]
15-
rate_limit: ClassVar[tuple[int, int] | None] = None
16-
description: ClassVar[str] = ""
20+
rate_limit: ClassVar[tuple[int, int]] = (50, 1)
21+
"""
22+
Tuple of (Number of Requests, Window in Seconds)
23+
"""
24+
25+
def get_rl_key(self, event) -> str:
26+
return f"{self.provider.value}:{event.project.organization_id}"
27+
28+
def is_ratelimited(self, event: Event) -> bool:
29+
rl_key = self.get_rl_key(event)
30+
limit, window = self.rate_limit
31+
if limit and window and ratelimits.backend.is_limited(rl_key, limit=limit, window=window):
32+
logger.info(
33+
"data_forwarding.skip_rate_limited",
34+
extra={
35+
"event_id": event.event_id,
36+
"issue_id": event.group_id,
37+
"project_id": event.project_id,
38+
"organization_id": event.project.organization_id,
39+
},
40+
)
41+
return True
42+
return False
43+
44+
def initialize_variables(self, event: Event, config: dict[str, Any]) -> None:
45+
"""This is only necessary for migrating Splunk plugin, needed for rate limiting"""
46+
return
1747

18-
@classmethod
1948
@abstractmethod
20-
def forward_event(cls, event: Event, data_forwarder_project: DataForwarderProject) -> bool:
21-
pass
49+
def forward_event(self, event: Event, payload: dict[str, Any], config: dict[str, Any]) -> bool:
50+
raise NotImplementedError
51+
52+
@abstractmethod
53+
def get_event_payload(self, event: Event, config: dict[str, Any]) -> dict[str, Any]:
54+
raise NotImplementedError
55+
56+
def post_process(self, event: Event, data_forwarder_project: DataForwarderProject) -> None:
57+
config = data_forwarder_project.get_config()
58+
if self.is_ratelimited(event):
59+
return
60+
61+
payload = self.get_event_payload(event=event, config=config)
62+
self.forward_event(event=event, payload=payload, config=config)

src/sentry/integrations/data_forwarding/segment/forwarder.py

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,18 @@
55

66
from sentry import VERSION, http
77
from sentry.integrations.data_forwarding.base import BaseDataForwarder
8-
from sentry.integrations.models.data_forwarder_project import DataForwarderProject
98
from sentry.integrations.types import DataForwarderProviderSlug
109
from sentry.services.eventstore.models import Event
1110

1211
logger = logging.getLogger(__name__)
1312

14-
DESCRIPTION = """
15-
Send Sentry events to Segment.
16-
17-
This integration allows you to forward Sentry error events to Segment for unified analytics.
18-
19-
Segment is a customer data platform (CDP) that helps you collect, clean, and control your customer data.
20-
"""
21-
2213

2314
class SegmentForwarder(BaseDataForwarder):
2415
provider = DataForwarderProviderSlug.SEGMENT
2516
rate_limit = (200, 1)
26-
description = DESCRIPTION
2717
endpoint: ClassVar[str] = "https://api.segment.io/v1/track"
2818

29-
@classmethod
30-
def validate_event(cls, event: Event) -> bool:
31-
# we currently only support errors
32-
if event.get_event_type() != "error":
33-
return False
34-
35-
# we avoid instantiating interfaces here as they're only going to be
36-
# used if there's a User present
37-
user_interface = event.interfaces.get("user")
38-
if not user_interface:
39-
return False
40-
41-
# if the user id is not present, we can't forward the event
42-
if not user_interface.id:
43-
return False
44-
45-
return True
46-
47-
@classmethod
48-
def get_event_payload(cls, event: Event) -> dict[str, Any]:
19+
def get_event_payload(self, event: Event, config: dict[str, Any]) -> dict[str, Any]:
4920
context = {"library": {"name": "sentry", "version": VERSION}}
5021

5122
props = {
@@ -95,43 +66,43 @@ def get_event_payload(cls, event: Event) -> dict[str, Any]:
9566
"timestamp": event.datetime.isoformat() + "Z",
9667
}
9768

98-
@classmethod
99-
def send_payload(
100-
cls,
69+
def forward_event(
70+
self,
10171
payload: dict[str, Any],
10272
config: dict[str, Any],
10373
event: Event,
104-
data_forwarder_project: DataForwarderProject,
10574
) -> bool:
75+
# we currently only support errors
76+
if event.get_event_type() != "error":
77+
return False
78+
79+
# we avoid instantiating interfaces here as they're only going to be
80+
# used if there's a User present
81+
user_interface = event.interfaces.get("user")
82+
if not user_interface:
83+
return False
84+
85+
# if the user id is not present, we can't forward the event
86+
if not user_interface.id:
87+
return False
88+
10689
write_key = config["write_key"]
90+
if not write_key:
91+
return False
10792

10893
try:
10994
with http.build_session() as session:
11095
response = session.post(
111-
cls.endpoint,
96+
self.endpoint,
11297
json=payload,
11398
auth=(write_key, ""),
11499
timeout=10,
115100
)
116101
response.raise_for_status()
117-
return True
118-
119102
except Exception:
120103
logger.exception(
121104
"segment.send_payload.error",
122-
extra={
123-
"event_id": event.event_id,
124-
"project_id": event.project_id,
125-
"data_forwarder_id": data_forwarder_project.data_forwarder_id,
126-
},
105+
extra={"event_id": event.event_id, "project_id": event.project_id},
127106
)
128107
return False
129-
130-
@classmethod
131-
def forward_event(cls, event: Event, data_forwarder_project: DataForwarderProject) -> bool:
132-
if not cls.validate_event(event):
133-
return False
134-
135-
config = data_forwarder_project.get_config()
136-
payload = cls.get_event_payload(event)
137-
return cls.send_payload(payload, config, event, data_forwarder_project)
108+
return True

0 commit comments

Comments
 (0)