Skip to content

Commit

Permalink
Merge branch 'tony/prometheus-metrics' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
tony-codecov committed Oct 28, 2024
2 parents d78ee7e + c691c6e commit f757384
Show file tree
Hide file tree
Showing 26 changed files with 333 additions and 174 deletions.
3 changes: 3 additions & 0 deletions codecov/settings_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@
"setup", "upload_throttling_enabled", default=True
)

HIDE_ALL_CODECOV_TOKENS = get_config("setup", "hide_all_codecov_tokens", default=False)


SENTRY_JWT_SHARED_SECRET = get_config(
"sentry", "jwt_shared_secret", default=None
) or get_config("setup", "sentry", "jwt_shared_secret", default=None)
Expand Down
23 changes: 23 additions & 0 deletions graphql_api/tests/test_owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
UnauthorizedGuestAccess,
)
from codecov_auth.models import GithubAppInstallation, OwnerProfile
from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
from plan.constants import PlanName, TrialStatus
from reports.tests.factories import CommitReportFactory, UploadFactory

Expand Down Expand Up @@ -426,6 +427,28 @@ def test_get_org_upload_token(self, mocker):
data = self.gql_request(query, owner=self.owner)
assert data["owner"]["orgUploadToken"] == "upload_token"

@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
def test_get_org_upload_token_hide_tokens_setting_owner_not_admin(self):
random_owner = OwnerFactory()
query = """{
owner(username: "%s") {
orgUploadToken
}
}
""" % (self.owner.username)
random_owner.organizations = [self.owner.ownerid]
random_owner.save()
data = self.gql_request(query, owner=random_owner)
assert data["owner"]["orgUploadToken"] == TOKEN_UNAVAILABLE

@patch("codecov_auth.commands.owner.owner.OwnerCommands.get_org_upload_token")
@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
def test_get_org_upload_token_hide_tokens_setting_owner_is_admin(self, mocker):
mocker.return_value = "upload_token"
query = query_repositories % (self.owner.username, "", "")
data = self.gql_request(query, owner=self.owner)
assert data["owner"]["orgUploadToken"] == "upload_token"

# Applies for old users that didn't get their owner profiles created w/ their owner
def test_when_owner_profile_doesnt_exist(self):
owner = OwnerFactory(username="no-profile-user")
Expand Down
68 changes: 68 additions & 0 deletions graphql_api/tests/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
RepositoryTokenFactory,
)

from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
from services.profiling import CriticalFile

from .helper import GraphQLTestHelper
Expand Down Expand Up @@ -73,6 +74,11 @@
"""


def mock_get_config_global_upload_tokens(*args):
if args == ("setup", "hide_all_codecov_tokens"):
return True


class TestFetchRepository(GraphQLTestHelper, TransactionTestCase):
def fetch_repository(self, name, fields=None):
data = self.gql_request(
Expand Down Expand Up @@ -683,6 +689,68 @@ def test_fetch_is_github_rate_limited_not_on_gh_service(self):

assert data["me"]["owner"]["repository"]["isGithubRateLimited"] == False

@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
def test_repo_upload_token_not_available_config_setting_owner_not_admin(self):
owner = OwnerFactory(service="gitlab")

repo = RepositoryFactory(
author=owner,
author__service="gitlab",
service_id=12345,
active=True,
)
new_owner = OwnerFactory(service="gitlab", organizations=[owner.ownerid])
new_owner.permission = [repo.repoid]
new_owner.save()
owner.admins = []

query = """
query {
owner(username: "%s") {
repository(name: "%s") {
... on Repository {
uploadToken
}
}
}
}
""" % (
owner.username,
repo.name,
)

data = self.gql_request(
query,
owner=new_owner,
variables={"name": repo.name},
provider="gitlab",
)

assert data["owner"]["repository"]["uploadToken"] == TOKEN_UNAVAILABLE

@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
def test_repo_upload_token_not_available_config_setting_owner_is_admin(self):
owner = OwnerFactory(service="gitlab")
repo = RepositoryFactory(
author=owner,
author__service="gitlab",
service_id=12345,
active=True,
)
owner.admins = [owner.ownerid]

data = self.gql_request(
query_repository
% """
uploadToken
""",
owner=owner,
variables={"name": repo.name},
provider="gitlab",
)

assert data["me"]["owner"]["repository"]["uploadToken"] != TOKEN_UNAVAILABLE

@patch("shared.rate_limits.determine_entity_redis_key")
@patch("shared.rate_limits.determine_if_entity_is_rate_limited")
@patch("logging.Logger.warning")
Expand Down
16 changes: 8 additions & 8 deletions graphql_api/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from unittest.mock import Mock, call, patch
from unittest.mock import Mock, patch

from ariadne import ObjectType, make_executable_schema
from ariadne.validation import cost_directive
Expand Down Expand Up @@ -201,11 +201,12 @@ async def test_query_metrics_extension_set_type_and_name_timeout(
assert extension.operation_type == "unknown_type"
assert extension.operation_name == "unknown_name"

@patch("sentry_sdk.metrics.incr")
@patch("graphql_api.views.GQL_REQUEST_MADE_COUNTER.labels")
@patch("graphql_api.views.GQL_ERROR_TYPE_COUNTER.labels")
@patch("graphql_api.views.AsyncGraphqlView._check_ratelimit")
@override_settings(DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=1000)
async def test_when_rate_limit_reached(
self, mocked_check_ratelimit, mocked_sentry_incr
self, mocked_check_ratelimit, mocked_error_counter, mocked_request_counter
):
schema = generate_cost_test_schema()
mocked_check_ratelimit.return_value = True
Expand All @@ -217,11 +218,10 @@ async def test_when_rate_limit_reached(
== "It looks like you've hit the rate limit of 1000 req/min. Try again later."
)

expected_calls = [
call("graphql.info.request_made", tags={"path": "/graphql/gh"}),
call("graphql.error.rate_limit", tags={"path": "/graphql/gh"}),
]
mocked_sentry_incr.assert_has_calls(expected_calls)
mocked_error_counter.assert_called_with(
error_type="rate_limit", path="/graphql/gh"
)
mocked_request_counter.assert_called_with(path="/graphql/gh")

@override_settings(
DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=0, GRAPHQL_RATE_LIMIT_ENABLED=False
Expand Down
11 changes: 11 additions & 0 deletions graphql_api/types/owner/owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import stripe
import yaml
from ariadne import ObjectType
from django.conf import settings
from graphql import GraphQLResolveInfo

import services.activation as activation
Expand Down Expand Up @@ -37,6 +38,7 @@
)
from graphql_api.types.enums import OrderingDirection, RepositoryOrdering
from graphql_api.types.errors.errors import NotFoundError, OwnerNotActivatedError
from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
from plan.constants import FREE_PLAN_REPRESENTATIONS, PlanData, PlanName
from plan.service import PlanService
from services.billing import BillingService
Expand Down Expand Up @@ -205,7 +207,16 @@ def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo) -> str:
def resolve_org_upload_token(
owner: Owner, info: GraphQLResolveInfo, **kwargs: Any
) -> str:
should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS
current_owner = info.context["request"].current_owner
command = info.context["executor"].get_command("owner")
if not current_owner:
is_owner_admin = False
else:
is_owner_admin = current_owner.is_admin(owner)
if should_hide_tokens and not is_owner_admin:
return TOKEN_UNAVAILABLE

return command.get_org_upload_token(owner)


Expand Down
13 changes: 13 additions & 0 deletions graphql_api/types/repository/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import shared.rate_limits as rate_limits
import yaml
from ariadne import ObjectType, UnionType
from django.conf import settings
from graphql.type.definition import GraphQLResolveInfo

from codecov.db import sync_to_async
Expand All @@ -24,6 +25,8 @@
from services.profiling import CriticalFile, ProfilingSummary
from services.redis_configuration import get_redis_connection

TOKEN_UNAVAILABLE = "Token Unavailable. Please contact your admin."

log = logging.getLogger(__name__)

repository_bindable = ObjectType("Repository")
Expand Down Expand Up @@ -68,6 +71,16 @@ def resolve_commit(repository: Repository, info: GraphQLResolveInfo, id):

@repository_bindable.field("uploadToken")
def resolve_upload_token(repository: Repository, info: GraphQLResolveInfo):
should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS

current_owner = info.context["request"].current_owner
if not current_owner:
is_current_user_admin = False
else:
is_current_user_admin = current_owner.is_admin(repository.author)

if should_hide_tokens and not is_current_user_admin:
return TOKEN_UNAVAILABLE
command = info.context["executor"].get_command("repository")
return command.get_upload_token(repository)

Expand Down
46 changes: 34 additions & 12 deletions graphql_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
)
from graphql import DocumentNode
from sentry_sdk import capture_exception
from sentry_sdk import metrics as sentry_metrics
from shared.metrics import Counter, Histogram
from shared.metrics import Counter, Histogram, inc_counter

from codecov.commands.exceptions import BaseException
from codecov.commands.executor import get_executor_from_request
Expand Down Expand Up @@ -51,6 +50,17 @@
buckets=[0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 5, 10, 30],
)

GQL_REQUEST_MADE_COUNTER = Counter(
"api_gql_requests_made",
"Total API GQL requests made",
["path"],
)

GQL_ERROR_TYPE_COUNTER = Counter(
"api_gql_errors",
"Number of times API GQL endpoint failed with an exception by type",
["error_type", "path"],
)

# covers named and 3 unnamed operations (see graphql_api/types/query/query.py)
GQL_TYPE_AND_NAME_PATTERN = r"^(query|mutation|subscription)(?:\(\$input:|) (\w+)(?:\(| \(|{| {|!)|^(?:{) (me|owner|config)(?:\(| |{)"
Expand Down Expand Up @@ -109,9 +119,13 @@ def request_started(self, context):
"""
self.set_type_and_name(query=context["clean_query"])
self.start_timestamp = time.perf_counter()
GQL_HIT_COUNTER.labels(
operation_type=self.operation_type, operation_name=self.operation_name
).inc()
inc_counter(
GQL_HIT_COUNTER,
labels=dict(
operation_type=self.operation_type,
operation_name=self.operation_name,
),
)

def request_finished(self, context):
"""
Expand Down Expand Up @@ -226,10 +240,12 @@ async def post(self, request, *args, **kwargs):
"user": request.user,
}
log.info("GraphQL Request", extra=log_data)
sentry_metrics.incr("graphql.info.request_made", tags={"path": req_path})

inc_counter(GQL_REQUEST_MADE_COUNTER, labels=dict(path=req_path))
if self._check_ratelimit(request=request):
sentry_metrics.incr("graphql.error.rate_limit", tags={"path": req_path})
inc_counter(
GQL_ERROR_TYPE_COUNTER,
labels=dict(error_type="rate_limit", path=req_path),
)
return JsonResponse(
data={
"status": 429,
Expand All @@ -250,7 +266,10 @@ async def post(self, request, *args, **kwargs):
data = json.loads(content)

if "errors" in data:
sentry_metrics.incr("graphql.error.all", tags={"path": req_path})
inc_counter(
GQL_ERROR_TYPE_COUNTER,
labels=dict(error_type="all", path=req_path),
)
try:
if data["errors"][0]["extensions"]["cost"]:
costs = data["errors"][0]["extensions"]["cost"]
Expand All @@ -262,9 +281,12 @@ async def post(self, request, *args, **kwargs):
request_body=req_body,
),
)
sentry_metrics.incr(
"graphql.error.query_cost_exceeded",
tags={"path": req_path},
inc_counter(
GQL_ERROR_TYPE_COUNTER,
labels=dict(
error_type="query_cost_exceeded",
path=req_path,
),
)
return HttpResponseBadRequest(
JsonResponse("Your query is too costly.")
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ freezegun==1.1.0
# via -r requirements.in
google-api-core[grpc]==2.11.1
# via
# google-api-core
# google-cloud-core
# google-cloud-pubsub
# google-cloud-storage
Expand Down Expand Up @@ -408,7 +409,9 @@ requests==2.32.3
# shared
# stripe
rfc3986[idna2008]==1.4.0
# via httpx
# via
# httpx
# rfc3986
rsa==4.7.2
# via google-auth
s3transfer==0.5.0
Expand Down
27 changes: 19 additions & 8 deletions upload/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,14 +790,15 @@ def get_version_from_headers(headers):
return "unknown-user-agent"


def generate_upload_sentry_metrics_tags(
def generate_upload_prometheus_metrics_labels(
action,
request,
is_shelter_request,
endpoint: Optional[str] = None,
repository: Optional[Repository] = None,
position: Optional[str] = None,
upload_version: Optional[str] = None,
include_empty_labels: bool = True,
):
metrics_tags = dict(
agent=get_agent_from_headers(request.headers),
Expand All @@ -806,13 +807,23 @@ def generate_upload_sentry_metrics_tags(
endpoint=endpoint,
is_using_shelter="yes" if is_shelter_request else "no",
)

repo_visibility = None
if repository:
metrics_tags["repo_visibility"] = (
"private" if repository.private is True else "public"
)
if position:
metrics_tags["position"] = position
if upload_version:
metrics_tags["upload_version"] = upload_version
repo_visibility = "private" if repository.private else "public"

optional_fields = {
"repo_visibility": repo_visibility,
"position": position,
"upload_version": upload_version,
}

metrics_tags.update(
{
field: value
for field, value in optional_fields.items()
if value or include_empty_labels
}
)

return metrics_tags
Loading

0 comments on commit f757384

Please sign in to comment.