Просмотр исходного кода

metrics(hybrid-cloud): Add more metrics for integration debugging (#58801)

This PR adds a few more metrics and removes the logging errors that
weren't too helpful.

A few notable things being added alongside that:
- atlassian connect code was untested, fixed that
- removed a few shared endpoints from being caught by the parser
Leander Rodrigues 1 год назад
Родитель
Сommit
709f515940

+ 15 - 9
src/sentry/api/endpoints/internal/integration_proxy.py

@@ -54,7 +54,7 @@ class InternalIntegrationProxyEndpoint(Endpoint):
         identifier = request.headers.get(PROXY_OI_HEADER)
         base_url = request.headers.get(PROXY_BASE_URL_HEADER)
         if signature is None or identifier is None or base_url is None:
-            logger.error("invalid_sender_headers", extra=self.log_extra)
+            logger.info("integration_proxy.invalid_sender_headers", extra=self.log_extra)
             return False
         is_valid = verify_subnet_signature(
             base_url=base_url,
@@ -64,7 +64,7 @@ class InternalIntegrationProxyEndpoint(Endpoint):
             provided_signature=signature,
         )
         if not is_valid:
-            logger.error("invalid_sender_signature", extra=self.log_extra)
+            logger.info("integration_proxy.invalid_sender_signature", extra=self.log_extra)
 
         return is_valid
 
@@ -77,7 +77,7 @@ class InternalIntegrationProxyEndpoint(Endpoint):
         # Get the organization integration
         org_integration_id = request.headers.get(PROXY_OI_HEADER)
         if org_integration_id is None:
-            logger.error("missing_org_integration", extra=self.log_extra)
+            logger.info("integration_proxy.missing_org_integration", extra=self.log_extra)
             return False
         self.log_extra["org_integration_id"] = org_integration_id
 
@@ -90,14 +90,14 @@ class InternalIntegrationProxyEndpoint(Endpoint):
             .first()
         )
         if self.org_integration is None:
-            logger.error("invalid_org_integration", extra=self.log_extra)
+            logger.info("integration_proxy.invalid_org_integration", extra=self.log_extra)
             return False
         self.log_extra["integration_id"] = self.org_integration.integration_id
 
         # Get the integration
         self.integration = self.org_integration.integration
         if not self.integration or self.integration.status is not ObjectStatus.ACTIVE:
-            logger.error("invalid_integration", extra=self.log_extra)
+            logger.info("integration_proxy.invalid_integration", extra=self.log_extra)
             return False
 
         # Get the integration client
@@ -108,7 +108,7 @@ class InternalIntegrationProxyEndpoint(Endpoint):
         client_class = self.client.__class__
         self.log_extra["client_type"] = client_class.__name__
         if not issubclass(client_class, IntegrationProxyClient):
-            logger.error("invalid_client", extra=self.log_extra)
+            logger.info("integration_proxy.invalid_client", extra=self.log_extra)
             return False
 
         return True
@@ -123,12 +123,12 @@ class InternalIntegrationProxyEndpoint(Endpoint):
 
         is_valid_sender = self._validate_sender(request=request)
         if not is_valid_sender:
-            metrics.incr("hc.integration_proxy.failure.invalid_sender")
+            metrics.incr("hybrid_cloud.integration_proxy.failure.invalid_sender", sample_rate=1.0)
             return False
 
         is_valid_request = self._validate_request(request=request)
         if not is_valid_request:
-            metrics.incr("hc.integration_proxy.failure.invalid_request")
+            metrics.incr("hybrid_cloud.integration_proxy.failure.invalid_request", sample_rate=1.0)
             return False
 
         return True
@@ -169,6 +169,8 @@ class InternalIntegrationProxyEndpoint(Endpoint):
         if not self._should_operate(request):
             raise Http404
 
+        metrics.incr("hybrid_cloud.integration_proxy.initialize", sample_rate=1.0)
+
         base_url = request.headers.get(PROXY_BASE_URL_HEADER)
         base_url = base_url.rstrip("/")
 
@@ -187,6 +189,10 @@ class InternalIntegrationProxyEndpoint(Endpoint):
                 request=request, full_url=full_url, headers=headers
             )
 
-        metrics.incr("hc.integration_proxy.success")
+        metrics.incr(
+            "hybrid_cloud.integration_proxy.complete.response_code",
+            tags={"status": response.status_code},
+            sample_rate=1.0,
+        )
         logger.info("proxy_success", extra=self.log_extra)
         return response

+ 4 - 0
src/sentry/integrations/jira/urls.py

@@ -16,10 +16,12 @@ urlpatterns = [
     re_path(
         r"^ui-hook/$",
         JiraSentryInstallationView.as_view(),
+        name="sentry-extensions-jira-ui-hook",
     ),
     re_path(
         r"^descriptor/$",
         JiraDescriptorEndpoint.as_view(),
+        name="sentry-extensions-descriptor",
     ),
     re_path(
         r"^installed/$",
@@ -29,6 +31,7 @@ urlpatterns = [
     re_path(
         r"^uninstalled/$",
         JiraSentryUninstalledWebhook.as_view(),
+        name="sentry-extensions-jira-uninstalled",
     ),
     re_path(
         r"^issue-updated/$",
@@ -43,6 +46,7 @@ urlpatterns = [
     re_path(
         r"^configure/$",
         JiraExtensionConfigurationView.as_view(),
+        name="sentry-extensions-jira-configuration",
     ),
     re_path(
         r"^issue/(?P<issue_key>[^\/]+)/$",

+ 3 - 1
src/sentry/integrations/vsts/urls.py

@@ -7,7 +7,9 @@ from .webhooks import WorkItemWebhook
 
 urlpatterns = [
     re_path(
-        r"^issue-updated/$", WorkItemWebhook.as_view(), name="sentry-extensions-vsts-issue-updated"
+        r"^issue-updated/$",
+        WorkItemWebhook.as_view(),
+        name="sentry-extensions-vsts-issue-updated",
     ),
     re_path(
         r"^search/(?P<organization_slug>[^\/]+)/(?P<integration_id>\d+)/$",

+ 21 - 7
src/sentry/middleware/integrations/classifications.py

@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING, List, Mapping, Type, cast
 from django.http import HttpRequest
 from django.http.response import HttpResponseBase
 
+from sentry.utils import metrics
+
 if TYPE_CHECKING:
     from sentry.middleware.integrations.integration_control import ResponseHandler
     from sentry.middleware.integrations.parsers.base import BaseRequestParser
@@ -54,8 +56,6 @@ class PluginClassification(BaseClassification):
 class IntegrationClassification(BaseClassification):
     integration_prefix = "/extensions/"
     """Prefix for all integration requests. See `src/sentry/web/urls.py`"""
-    setup_suffix = "/setup/"
-    """Suffix for PipelineAdvancerView on installation. See `src/sentry/web/urls.py`"""
     logger = logging.getLogger(f"{__name__}.integration")
 
     @property
@@ -98,8 +98,14 @@ class IntegrationClassification(BaseClassification):
         return result[1] if result else None
 
     def should_operate(self, request: HttpRequest) -> bool:
-        return request.path.startswith(self.integration_prefix) and not request.path.endswith(
-            self.setup_suffix
+        return (
+            # Must start with the integration request prefix...
+            request.path.startswith(self.integration_prefix)
+            # Not have the suffix for PipelineAdvancerView (See urls.py)
+            and not request.path.endswith("/setup/")
+            # or match the routes for integrationOrganizationLink page (See routes.tsx)
+            and not request.path.endswith("/link/")
+            and not request.path.startswith("/extensions/external-install/")
         )
 
     def get_response(self, request: HttpRequest) -> HttpResponseBase:
@@ -110,7 +116,7 @@ class IntegrationClassification(BaseClassification):
         parser_class = self.integration_parsers.get(provider)
         if not parser_class:
             self.logger.error(
-                "unknown_provider",
+                "integration_control.unknown_provider",
                 extra={"path": request.path, "provider": provider},
             )
             return self.response_handler(request)
@@ -119,5 +125,13 @@ class IntegrationClassification(BaseClassification):
             request=request,
             response_handler=self.response_handler,
         )
-        self.logger.info(f"routing_request.{parser.provider}", extra={"path": request.path})
-        return parser.get_response()
+        self.logger.info(
+            f"integration_control.routing_request.{parser.provider}", extra={"path": request.path}
+        )
+        response = parser.get_response()
+        metrics.incr(
+            f"hybrid_cloud.integration_control.integration.{parser.provider}",
+            tags={"url_name": parser.match.url_name, "status_code": response.status_code},
+            sample_rate=1.0,
+        )
+        return response

+ 2 - 2
src/sentry/middleware/integrations/parsers/base.py

@@ -158,7 +158,7 @@ class BaseRequestParser(abc.ABC):
         if not integration:
             integration = self.get_integration_from_request()
         if not integration:
-            logger.error("no_integration", extra={"path": self.request.path})
+            logger.info(f"{self.provider}.no_integration", extra={"path": self.request.path})
             return []
         organization_integrations = OrganizationIntegration.objects.filter(
             integration_id=integration.id
@@ -175,7 +175,7 @@ class BaseRequestParser(abc.ABC):
         if not organizations:
             organizations = self.get_organizations_from_integration()
         if not organizations:
-            logger.error("no_organizations", extra={"path": self.request.path})
+            logger.info(f"{self.provider}.no_organizations", extra={"path": self.request.path})
             return []
 
         return [get_region_for_organization(organization.slug) for organization in organizations]

+ 3 - 3
src/sentry/middleware/integrations/parsers/bitbucket.py

@@ -25,7 +25,7 @@ class BitbucketRequestParser(BaseRequestParser):
         organization_id = self.match.kwargs.get("organization_id")
         logging_extra: dict[str, Any] = {"path": self.request.path}
         if not organization_id:
-            logger.info("no_organization_id", extra=logging_extra)
+            logger.info(f"{self.provider}.no_organization_id", extra=logging_extra)
             return self.get_response_from_control_silo()
 
         try:
@@ -35,7 +35,7 @@ class BitbucketRequestParser(BaseRequestParser):
         except OrganizationMapping.DoesNotExist as e:
             logging_extra["error"] = str(e)
             logging_extra["organization_id"] = organization_id
-            logger.error("no_mapping", extra=logging_extra)
+            logger.info(f"{self.provider}.no_mapping", extra=logging_extra)
             return self.get_response_from_control_silo()
 
         try:
@@ -43,7 +43,7 @@ class BitbucketRequestParser(BaseRequestParser):
         except RegionResolutionError as e:
             logging_extra["error"] = str(e)
             logging_extra["mapping_id"] = mapping.id
-            logger.error("no_region", extra=logging_extra)
+            logger.info(f"{self.provider}.no_region", extra=logging_extra)
             return self.get_response_from_control_silo()
         return self.get_response_from_outbox_creation(regions=[region])
 

+ 1 - 1
src/sentry/middleware/integrations/parsers/github.py

@@ -44,7 +44,7 @@ class GithubRequestParser(BaseRequestParser):
         if self.view_class == self.webhook_endpoint:
             regions = self.get_regions_from_organizations()
             if len(regions) == 0:
-                logger.error("no_regions", extra={"path": self.request.path})
+                logger.info(f"{self.provider}.no_regions", extra={"path": self.request.path})
                 return self.get_response_from_control_silo()
             return self.get_response_from_outbox_creation(regions=regions)
         return self.get_response_from_control_silo()

+ 1 - 1
src/sentry/middleware/integrations/parsers/gitlab.py

@@ -68,7 +68,7 @@ class GitlabRequestParser(BaseRequestParser, GitlabWebhookMixin):
 
         regions = self.get_regions_from_organizations()
         if len(regions) == 0:
-            logger.error("no_regions", extra={"path": self.request.path})
+            logger.info(f"{self.provider}.no_regions", extra={"path": self.request.path})
             return self.get_response_from_control_silo()
 
         return self.get_response_from_outbox_creation(regions=regions)

+ 10 - 3
src/sentry/middleware/integrations/parsers/jira.py

@@ -2,6 +2,8 @@ from __future__ import annotations
 
 import logging
 
+import sentry_sdk
+
 from sentry.integrations.jira.endpoints import JiraDescriptorEndpoint, JiraSearchEndpoint
 from sentry.integrations.jira.views import (
     JiraExtensionConfigurationView,
@@ -44,7 +46,7 @@ class JiraRequestParser(BaseRequestParser):
         try:
             return parse_integration_from_request(request=self.request, provider=self.provider)
         except AtlassianConnectValidationError as e:
-            logger.error("auth_invalid", extra={"error": e, "path": self.request.path})
+            sentry_sdk.capture_exception(e)
         return None
 
     def get_response(self):
@@ -53,13 +55,16 @@ class JiraRequestParser(BaseRequestParser):
 
         regions = self.get_regions_from_organizations()
         if len(regions) == 0:
-            logger.error("no_regions", extra={"path": self.request.path})
+            logger.info(f"{self.provider}.no_regions", extra={"path": self.request.path})
             return self.get_response_from_control_silo()
 
         if len(regions) > 1:
             # Since Jira is region_restricted (see JiraIntegrationProvider) we can just pick the
             # first region to forward along to.
-            logger.error("too_many_regions", extra={"path": self.request.path, "regions": regions})
+            logger.info(
+                f"{self.provider}.too_many_regions",
+                extra={"path": self.request.path, "regions": regions},
+            )
             return self.get_response_from_control_silo()
 
         if self.view_class in self.immediate_response_region_classes:
@@ -67,3 +72,5 @@ class JiraRequestParser(BaseRequestParser):
 
         if self.view_class in self.outbox_response_region_classes:
             return self.get_response_from_outbox_creation(regions=regions)
+
+        return self.get_response_from_control_silo()

+ 1 - 1
src/sentry/middleware/integrations/parsers/jira_server.py

@@ -21,7 +21,7 @@ class JiraServerRequestParser(BaseRequestParser):
         try:
             integration = get_integration_from_token(token)
         except ValueError as e:
-            logger.error("no_integration", extra={"error": str(e)})
+            logger.info(f"{self.provider}.no_integration", extra={"error": str(e)})
             return self.get_response_from_control_silo()
         organizations = self.get_organizations_from_integration(integration=integration)
         regions = self.get_regions_from_organizations(organizations=organizations)

Некоторые файлы не были показаны из-за большого количества измененных файлов