Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit f757384

Browse files
committed
Merge branch 'tony/prometheus-metrics' into staging
2 parents d78ee7e + c691c6e commit f757384

26 files changed

+333
-174
lines changed

codecov/settings_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@
356356
"setup", "upload_throttling_enabled", default=True
357357
)
358358

359+
HIDE_ALL_CODECOV_TOKENS = get_config("setup", "hide_all_codecov_tokens", default=False)
360+
361+
359362
SENTRY_JWT_SHARED_SECRET = get_config(
360363
"sentry", "jwt_shared_secret", default=None
361364
) or get_config("setup", "sentry", "jwt_shared_secret", default=None)

graphql_api/tests/test_owner.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
UnauthorizedGuestAccess,
2828
)
2929
from codecov_auth.models import GithubAppInstallation, OwnerProfile
30+
from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
3031
from plan.constants import PlanName, TrialStatus
3132
from reports.tests.factories import CommitReportFactory, UploadFactory
3233

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

430+
@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
431+
def test_get_org_upload_token_hide_tokens_setting_owner_not_admin(self):
432+
random_owner = OwnerFactory()
433+
query = """{
434+
owner(username: "%s") {
435+
orgUploadToken
436+
}
437+
}
438+
""" % (self.owner.username)
439+
random_owner.organizations = [self.owner.ownerid]
440+
random_owner.save()
441+
data = self.gql_request(query, owner=random_owner)
442+
assert data["owner"]["orgUploadToken"] == TOKEN_UNAVAILABLE
443+
444+
@patch("codecov_auth.commands.owner.owner.OwnerCommands.get_org_upload_token")
445+
@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
446+
def test_get_org_upload_token_hide_tokens_setting_owner_is_admin(self, mocker):
447+
mocker.return_value = "upload_token"
448+
query = query_repositories % (self.owner.username, "", "")
449+
data = self.gql_request(query, owner=self.owner)
450+
assert data["owner"]["orgUploadToken"] == "upload_token"
451+
429452
# Applies for old users that didn't get their owner profiles created w/ their owner
430453
def test_when_owner_profile_doesnt_exist(self):
431454
owner = OwnerFactory(username="no-profile-user")

graphql_api/tests/test_repository.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
RepositoryTokenFactory,
1212
)
1313

14+
from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
1415
from services.profiling import CriticalFile
1516

1617
from .helper import GraphQLTestHelper
@@ -73,6 +74,11 @@
7374
"""
7475

7576

77+
def mock_get_config_global_upload_tokens(*args):
78+
if args == ("setup", "hide_all_codecov_tokens"):
79+
return True
80+
81+
7682
class TestFetchRepository(GraphQLTestHelper, TransactionTestCase):
7783
def fetch_repository(self, name, fields=None):
7884
data = self.gql_request(
@@ -683,6 +689,68 @@ def test_fetch_is_github_rate_limited_not_on_gh_service(self):
683689

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

692+
@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
693+
def test_repo_upload_token_not_available_config_setting_owner_not_admin(self):
694+
owner = OwnerFactory(service="gitlab")
695+
696+
repo = RepositoryFactory(
697+
author=owner,
698+
author__service="gitlab",
699+
service_id=12345,
700+
active=True,
701+
)
702+
new_owner = OwnerFactory(service="gitlab", organizations=[owner.ownerid])
703+
new_owner.permission = [repo.repoid]
704+
new_owner.save()
705+
owner.admins = []
706+
707+
query = """
708+
query {
709+
owner(username: "%s") {
710+
repository(name: "%s") {
711+
... on Repository {
712+
uploadToken
713+
}
714+
}
715+
}
716+
}
717+
""" % (
718+
owner.username,
719+
repo.name,
720+
)
721+
722+
data = self.gql_request(
723+
query,
724+
owner=new_owner,
725+
variables={"name": repo.name},
726+
provider="gitlab",
727+
)
728+
729+
assert data["owner"]["repository"]["uploadToken"] == TOKEN_UNAVAILABLE
730+
731+
@override_settings(HIDE_ALL_CODECOV_TOKENS=True)
732+
def test_repo_upload_token_not_available_config_setting_owner_is_admin(self):
733+
owner = OwnerFactory(service="gitlab")
734+
repo = RepositoryFactory(
735+
author=owner,
736+
author__service="gitlab",
737+
service_id=12345,
738+
active=True,
739+
)
740+
owner.admins = [owner.ownerid]
741+
742+
data = self.gql_request(
743+
query_repository
744+
% """
745+
uploadToken
746+
""",
747+
owner=owner,
748+
variables={"name": repo.name},
749+
provider="gitlab",
750+
)
751+
752+
assert data["me"]["owner"]["repository"]["uploadToken"] != TOKEN_UNAVAILABLE
753+
686754
@patch("shared.rate_limits.determine_entity_redis_key")
687755
@patch("shared.rate_limits.determine_if_entity_is_rate_limited")
688756
@patch("logging.Logger.warning")

graphql_api/tests/test_views.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from unittest.mock import Mock, call, patch
2+
from unittest.mock import Mock, patch
33

44
from ariadne import ObjectType, make_executable_schema
55
from ariadne.validation import cost_directive
@@ -201,11 +201,12 @@ async def test_query_metrics_extension_set_type_and_name_timeout(
201201
assert extension.operation_type == "unknown_type"
202202
assert extension.operation_name == "unknown_name"
203203

204-
@patch("sentry_sdk.metrics.incr")
204+
@patch("graphql_api.views.GQL_REQUEST_MADE_COUNTER.labels")
205+
@patch("graphql_api.views.GQL_ERROR_TYPE_COUNTER.labels")
205206
@patch("graphql_api.views.AsyncGraphqlView._check_ratelimit")
206207
@override_settings(DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=1000)
207208
async def test_when_rate_limit_reached(
208-
self, mocked_check_ratelimit, mocked_sentry_incr
209+
self, mocked_check_ratelimit, mocked_error_counter, mocked_request_counter
209210
):
210211
schema = generate_cost_test_schema()
211212
mocked_check_ratelimit.return_value = True
@@ -217,11 +218,10 @@ async def test_when_rate_limit_reached(
217218
== "It looks like you've hit the rate limit of 1000 req/min. Try again later."
218219
)
219220

220-
expected_calls = [
221-
call("graphql.info.request_made", tags={"path": "/graphql/gh"}),
222-
call("graphql.error.rate_limit", tags={"path": "/graphql/gh"}),
223-
]
224-
mocked_sentry_incr.assert_has_calls(expected_calls)
221+
mocked_error_counter.assert_called_with(
222+
error_type="rate_limit", path="/graphql/gh"
223+
)
224+
mocked_request_counter.assert_called_with(path="/graphql/gh")
225225

226226
@override_settings(
227227
DEBUG=False, GRAPHQL_RATE_LIMIT_RPM=0, GRAPHQL_RATE_LIMIT_ENABLED=False

graphql_api/types/owner/owner.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import stripe
77
import yaml
88
from ariadne import ObjectType
9+
from django.conf import settings
910
from graphql import GraphQLResolveInfo
1011

1112
import services.activation as activation
@@ -37,6 +38,7 @@
3738
)
3839
from graphql_api.types.enums import OrderingDirection, RepositoryOrdering
3940
from graphql_api.types.errors.errors import NotFoundError, OwnerNotActivatedError
41+
from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE
4042
from plan.constants import FREE_PLAN_REPRESENTATIONS, PlanData, PlanName
4143
from plan.service import PlanService
4244
from services.billing import BillingService
@@ -205,7 +207,16 @@ def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo) -> str:
205207
def resolve_org_upload_token(
206208
owner: Owner, info: GraphQLResolveInfo, **kwargs: Any
207209
) -> str:
210+
should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS
211+
current_owner = info.context["request"].current_owner
208212
command = info.context["executor"].get_command("owner")
213+
if not current_owner:
214+
is_owner_admin = False
215+
else:
216+
is_owner_admin = current_owner.is_admin(owner)
217+
if should_hide_tokens and not is_owner_admin:
218+
return TOKEN_UNAVAILABLE
219+
209220
return command.get_org_upload_token(owner)
210221

211222

graphql_api/types/repository/repository.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shared.rate_limits as rate_limits
66
import yaml
77
from ariadne import ObjectType, UnionType
8+
from django.conf import settings
89
from graphql.type.definition import GraphQLResolveInfo
910

1011
from codecov.db import sync_to_async
@@ -24,6 +25,8 @@
2425
from services.profiling import CriticalFile, ProfilingSummary
2526
from services.redis_configuration import get_redis_connection
2627

28+
TOKEN_UNAVAILABLE = "Token Unavailable. Please contact your admin."
29+
2730
log = logging.getLogger(__name__)
2831

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

6972
@repository_bindable.field("uploadToken")
7073
def resolve_upload_token(repository: Repository, info: GraphQLResolveInfo):
74+
should_hide_tokens = settings.HIDE_ALL_CODECOV_TOKENS
75+
76+
current_owner = info.context["request"].current_owner
77+
if not current_owner:
78+
is_current_user_admin = False
79+
else:
80+
is_current_user_admin = current_owner.is_admin(repository.author)
81+
82+
if should_hide_tokens and not is_current_user_admin:
83+
return TOKEN_UNAVAILABLE
7184
command = info.context["executor"].get_command("repository")
7285
return command.get_upload_token(repository)
7386

graphql_api/views.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
)
2020
from graphql import DocumentNode
2121
from sentry_sdk import capture_exception
22-
from sentry_sdk import metrics as sentry_metrics
23-
from shared.metrics import Counter, Histogram
22+
from shared.metrics import Counter, Histogram, inc_counter
2423

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

53+
GQL_REQUEST_MADE_COUNTER = Counter(
54+
"api_gql_requests_made",
55+
"Total API GQL requests made",
56+
["path"],
57+
)
58+
59+
GQL_ERROR_TYPE_COUNTER = Counter(
60+
"api_gql_errors",
61+
"Number of times API GQL endpoint failed with an exception by type",
62+
["error_type", "path"],
63+
)
5464

5565
# covers named and 3 unnamed operations (see graphql_api/types/query/query.py)
5666
GQL_TYPE_AND_NAME_PATTERN = r"^(query|mutation|subscription)(?:\(\$input:|) (\w+)(?:\(| \(|{| {|!)|^(?:{) (me|owner|config)(?:\(| |{)"
@@ -109,9 +119,13 @@ def request_started(self, context):
109119
"""
110120
self.set_type_and_name(query=context["clean_query"])
111121
self.start_timestamp = time.perf_counter()
112-
GQL_HIT_COUNTER.labels(
113-
operation_type=self.operation_type, operation_name=self.operation_name
114-
).inc()
122+
inc_counter(
123+
GQL_HIT_COUNTER,
124+
labels=dict(
125+
operation_type=self.operation_type,
126+
operation_name=self.operation_name,
127+
),
128+
)
115129

116130
def request_finished(self, context):
117131
"""
@@ -226,10 +240,12 @@ async def post(self, request, *args, **kwargs):
226240
"user": request.user,
227241
}
228242
log.info("GraphQL Request", extra=log_data)
229-
sentry_metrics.incr("graphql.info.request_made", tags={"path": req_path})
230-
243+
inc_counter(GQL_REQUEST_MADE_COUNTER, labels=dict(path=req_path))
231244
if self._check_ratelimit(request=request):
232-
sentry_metrics.incr("graphql.error.rate_limit", tags={"path": req_path})
245+
inc_counter(
246+
GQL_ERROR_TYPE_COUNTER,
247+
labels=dict(error_type="rate_limit", path=req_path),
248+
)
233249
return JsonResponse(
234250
data={
235251
"status": 429,
@@ -250,7 +266,10 @@ async def post(self, request, *args, **kwargs):
250266
data = json.loads(content)
251267

252268
if "errors" in data:
253-
sentry_metrics.incr("graphql.error.all", tags={"path": req_path})
269+
inc_counter(
270+
GQL_ERROR_TYPE_COUNTER,
271+
labels=dict(error_type="all", path=req_path),
272+
)
254273
try:
255274
if data["errors"][0]["extensions"]["cost"]:
256275
costs = data["errors"][0]["extensions"]["cost"]
@@ -262,9 +281,12 @@ async def post(self, request, *args, **kwargs):
262281
request_body=req_body,
263282
),
264283
)
265-
sentry_metrics.incr(
266-
"graphql.error.query_cost_exceeded",
267-
tags={"path": req_path},
284+
inc_counter(
285+
GQL_ERROR_TYPE_COUNTER,
286+
labels=dict(
287+
error_type="query_cost_exceeded",
288+
path=req_path,
289+
),
268290
)
269291
return HttpResponseBadRequest(
270292
JsonResponse("Your query is too costly.")

requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ freezegun==1.1.0
177177
# via -r requirements.in
178178
google-api-core[grpc]==2.11.1
179179
# via
180+
# google-api-core
180181
# google-cloud-core
181182
# google-cloud-pubsub
182183
# google-cloud-storage
@@ -408,7 +409,9 @@ requests==2.32.3
408409
# shared
409410
# stripe
410411
rfc3986[idna2008]==1.4.0
411-
# via httpx
412+
# via
413+
# httpx
414+
# rfc3986
412415
rsa==4.7.2
413416
# via google-auth
414417
s3transfer==0.5.0

upload/helpers.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -790,14 +790,15 @@ def get_version_from_headers(headers):
790790
return "unknown-user-agent"
791791

792792

793-
def generate_upload_sentry_metrics_tags(
793+
def generate_upload_prometheus_metrics_labels(
794794
action,
795795
request,
796796
is_shelter_request,
797797
endpoint: Optional[str] = None,
798798
repository: Optional[Repository] = None,
799799
position: Optional[str] = None,
800800
upload_version: Optional[str] = None,
801+
include_empty_labels: bool = True,
801802
):
802803
metrics_tags = dict(
803804
agent=get_agent_from_headers(request.headers),
@@ -806,13 +807,23 @@ def generate_upload_sentry_metrics_tags(
806807
endpoint=endpoint,
807808
is_using_shelter="yes" if is_shelter_request else "no",
808809
)
810+
811+
repo_visibility = None
809812
if repository:
810-
metrics_tags["repo_visibility"] = (
811-
"private" if repository.private is True else "public"
812-
)
813-
if position:
814-
metrics_tags["position"] = position
815-
if upload_version:
816-
metrics_tags["upload_version"] = upload_version
813+
repo_visibility = "private" if repository.private else "public"
814+
815+
optional_fields = {
816+
"repo_visibility": repo_visibility,
817+
"position": position,
818+
"upload_version": upload_version,
819+
}
820+
821+
metrics_tags.update(
822+
{
823+
field: value
824+
for field, value in optional_fields.items()
825+
if value or include_empty_labels
826+
}
827+
)
817828

818829
return metrics_tags

0 commit comments

Comments
 (0)