Skip to content

Commit d951a74

Browse files
committed
Track metrics by instance URL to support multiple service instances
1 parent 7c50058 commit d951a74

File tree

6 files changed

+222
-208
lines changed

6 files changed

+222
-208
lines changed

ogr/metrics.py

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
Metrics tracking for OGR API calls.
66
77
This module provides a lightweight metrics tracking system to count
8-
API calls per service type and namespace. Metrics are stored in-memory
8+
API calls per instance URL and namespace. Metrics are stored in-memory
99
and are NOT thread-safe or process-synchronized.
1010
1111
The track_ogr_request decorator can be applied to GitProject methods
12-
to automatically track API calls.
12+
to automatically track API calls. The instance URL uniquely identifies
13+
the service instance (e.g., github.com, gitlab.com, gitlab.example.com).
1314
"""
1415

16+
from __future__ import annotations
17+
1518
import functools
1619
import logging
1720
from collections import defaultdict
18-
from typing import Any, Callable, TypeVar, cast
21+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
22+
23+
if TYPE_CHECKING:
24+
from ogr.abstract import GitProject
1925

2026
logger = logging.getLogger(__name__)
2127

@@ -24,7 +30,7 @@
2430

2531
class RequestMetricsTracker:
2632
"""
27-
Tracker for counting API calls per namespace and service type.
33+
Tracker for counting API calls per instance URL and namespace.
2834
2935
Note: This class is NOT thread-safe and does NOT synchronize across processes.
3036
In a multi-worker environment, each worker maintains its own metrics, and some
@@ -38,25 +44,25 @@ def __init__(self):
3844

3945
def record_request(
4046
self,
41-
service_type: str,
47+
instance_url: str,
4248
namespace: str,
4349
) -> None:
4450
"""
45-
Record an API request for a given service type and namespace.
51+
Record an API request for a given instance URL and namespace.
4652
4753
Args:
48-
service_type: The service type (e.g., "github", "gitlab", "pagure")
54+
instance_url: The service instance URL (e.g., "https://github.com", "https://gitlab.com")
4955
namespace: The namespace (e.g., "packit", "rpms")
5056
"""
51-
key = (service_type, namespace)
57+
key = (instance_url, namespace)
5258
self._counts[key] += 1
5359

5460
def get_all_counts(self) -> dict[tuple[str, str], int]:
5561
"""
5662
Get all request counts.
5763
5864
Returns:
59-
Dictionary mapping (service_type, namespace) tuples to counts.
65+
Dictionary mapping (instance_url, namespace) tuples to counts.
6066
"""
6167
return {key: count for key, count in self._counts.items() if count > 0}
6268

@@ -78,60 +84,58 @@ def get_metrics_tracker() -> RequestMetricsTracker:
7884
return _metrics_tracker
7985

8086

81-
def record_ogr_request(service_type: str, namespace: str) -> None:
87+
def record_ogr_request(instance_url: str, namespace: str) -> None:
8288
"""
8389
Record an ogr API request.
8490
8591
This is a convenience function that uses the global metrics tracker.
8692
8793
Args:
88-
service_type: The service type (e.g., "github", "gitlab", "pagure")
94+
instance_url: The service instance URL (e.g., "https://github.com", "https://gitlab.com")
8995
namespace: The namespace (e.g., "packit-service", "rpms")
9096
"""
91-
_metrics_tracker.record_request(service_type, namespace)
97+
_metrics_tracker.record_request(instance_url, namespace)
9298

9399

94-
def track_ogr_request(service_type: str) -> Callable[[F], F]:
100+
def track_ogr_request(func: F) -> F:
95101
"""
96102
Decorator to track ogr API method calls.
97103
98104
The decorated method must be called on a GitProject instance
99-
(which has `namespace` and `service` attributes).
105+
(which has `namespace`, `repo`, and `service` attributes).
100106
101107
For Pagure projects, the namespace is combined with the repo name
102108
(e.g., "rpms/python-requests") to provide more granular metrics.
103109
104-
Args:
105-
service_type: The service type (e.g., "github", "gitlab", "pagure")
110+
The instance URL is extracted from the service to distinguish between
111+
different instances (e.g., multiple GitLab or Forgejo instances).
106112
107113
Example:
108-
@track_ogr_request("github")
114+
@track_ogr_request
109115
def get_issues(self):
110116
...
111117
"""
112118

113-
def decorator(func: F) -> F:
114-
@functools.wraps(func)
115-
def wrapper(self: object, *args: Any, **kwargs: Any) -> Any:
116-
namespace = getattr(self, "namespace", None)
117-
if namespace:
118-
try:
119-
# For Pagure, append the repo name to the namespace
120-
# to get more granular metrics (e.g., "rpms/python-requests")
121-
if service_type == "pagure":
122-
repo = getattr(self, "repo", None)
123-
if repo:
124-
namespace = f"{namespace}/{repo}"
119+
@functools.wraps(func)
120+
def wrapper(self: GitProject, *args: Any, **kwargs: Any) -> Any:
121+
try:
122+
from ogr.services.pagure import PagureService
123+
124+
namespace = self.namespace
125+
instance_url = self.service.instance_url
125126

126-
record_ogr_request(service_type, namespace)
127-
except Exception as e:
128-
logger.debug(f"Failed to record metrics: {e}")
127+
# For Pagure, append the repo name to the namespace
128+
# to get more granular metrics (e.g., "rpms/python-requests")
129+
if isinstance(self.service, PagureService) and self.repo:
130+
namespace = f"{namespace}/{self.repo}"
129131

130-
return func(self, *args, **kwargs)
132+
record_ogr_request(instance_url, namespace)
133+
except Exception as e:
134+
logger.debug(f"Failed to record metrics: {e}")
131135

132-
return cast(F, wrapper)
136+
return func(self, *args, **kwargs)
133137

134-
return decorator
138+
return cast(F, wrapper)
135139

136140

137141
__all__ = [

ogr/services/forgejo/project.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def description(self, new_description: str) -> None:
122122
def delete(self) -> None:
123123
self.partial_api(self.api.repo_delete)()
124124

125-
@track_ogr_request("forgejo")
125+
@track_ogr_request
126126
def exists(self) -> bool:
127127
try:
128128
_ = self.forgejo_repo
@@ -162,7 +162,7 @@ def parent(self) -> Optional["GitProject"]:
162162
def has_issues(self) -> bool:
163163
return self.forgejo_repo.has_issues
164164

165-
@track_ogr_request("forgejo")
165+
@track_ogr_request
166166
def get_branches(self) -> Iterable[str]:
167167
return (
168168
branch.name
@@ -175,7 +175,7 @@ def get_branches(self) -> Iterable[str]:
175175
def default_branch(self) -> str:
176176
return self.forgejo_repo.default_branch
177177

178-
@track_ogr_request("forgejo")
178+
@track_ogr_request
179179
def get_commits(self, ref: Optional[str] = None) -> Iterable[str]:
180180
return (
181181
commit.sha
@@ -324,7 +324,7 @@ def remove_user(self, user: str) -> None:
324324
def request_access(self) -> None:
325325
raise OperationNotSupported("Not possible on Forgejo")
326326

327-
@track_ogr_request("forgejo")
327+
@track_ogr_request
328328
@indirect(ForgejoIssue.get_list)
329329
def get_issue_list(
330330
self,
@@ -335,12 +335,12 @@ def get_issue_list(
335335
) -> list["Issue"]:
336336
pass
337337

338-
@track_ogr_request("forgejo")
338+
@track_ogr_request
339339
@indirect(ForgejoIssue.get)
340340
def get_issue(self, issue_id: int) -> "Issue":
341341
pass
342342

343-
@track_ogr_request("forgejo")
343+
@track_ogr_request
344344
@indirect(ForgejoIssue.create)
345345
def create_issue(
346346
self,
@@ -352,12 +352,12 @@ def create_issue(
352352
) -> Issue:
353353
pass
354354

355-
@track_ogr_request("forgejo")
355+
@track_ogr_request
356356
@indirect(ForgejoPullRequest.get_list)
357357
def get_pr_list(self, status: PRStatus = PRStatus.open) -> Iterable["PullRequest"]:
358358
pass
359359

360-
@track_ogr_request("forgejo")
360+
@track_ogr_request
361361
@indirect(ForgejoPullRequest.get)
362362
def get_pr(self, pr_id: int) -> "PullRequest":
363363
pass
@@ -381,7 +381,7 @@ def get_pr_files_diff(
381381
# https://github.com/packit/ogr/issues/895
382382
raise NotImplementedError()
383383

384-
@track_ogr_request("forgejo")
384+
@track_ogr_request
385385
def get_tags(self) -> Iterable["GitTag"]:
386386
return (
387387
GitTag(
@@ -391,14 +391,14 @@ def get_tags(self) -> Iterable["GitTag"]:
391391
for tag in paginate(self.partial_api(self.api.repo_list_tags))
392392
)
393393

394-
@track_ogr_request("forgejo")
394+
@track_ogr_request
395395
def get_sha_from_tag(self, tag_name: str) -> str:
396396
return self.partial_api(
397397
self.api.repo_get_tag,
398398
tag=tag_name,
399399
)().commit.sha
400400

401-
@track_ogr_request("forgejo")
401+
@track_ogr_request
402402
@indirect(ForgejoRelease.get)
403403
def get_release(
404404
self,
@@ -408,17 +408,17 @@ def get_release(
408408
) -> Release:
409409
pass
410410

411-
@track_ogr_request("forgejo")
411+
@track_ogr_request
412412
@indirect(ForgejoRelease.get_latest)
413413
def get_latest_release(self) -> Optional[Release]:
414414
pass
415415

416-
@track_ogr_request("forgejo")
416+
@track_ogr_request
417417
@indirect(ForgejoRelease.get_list)
418418
def get_releases(self) -> list[Release]:
419419
pass
420420

421-
@track_ogr_request("forgejo")
421+
@track_ogr_request
422422
@indirect(ForgejoRelease.create)
423423
def create_release(
424424
self,
@@ -429,7 +429,7 @@ def create_release(
429429
) -> Release:
430430
pass
431431

432-
@track_ogr_request("forgejo")
432+
@track_ogr_request
433433
@indirect(ForgejoPullRequest.create)
434434
def create_pr(
435435
self,
@@ -456,7 +456,7 @@ def get_commit_comments(self, commit: str) -> list[CommitComment]:
456456
def get_commit_comment(self, commit_sha: str, comment_id: int) -> CommitComment:
457457
raise OperationNotSupported("Forgejo doesn't support commit comments")
458458

459-
@track_ogr_request("forgejo")
459+
@track_ogr_request
460460
@indirect(ForgejoCommitFlag.set)
461461
def set_commit_status(
462462
self,
@@ -469,7 +469,7 @@ def set_commit_status(
469469
) -> "CommitFlag":
470470
pass
471471

472-
@track_ogr_request("forgejo")
472+
@track_ogr_request
473473
@indirect(ForgejoCommitFlag.get)
474474
def get_commit_statuses(self, commit: str) -> Iterable["CommitFlag"]:
475475
pass
@@ -480,7 +480,7 @@ def get_git_urls(self) -> dict[str, str]:
480480
"ssh": self.forgejo_repo.ssh_url,
481481
}
482482

483-
@track_ogr_request("forgejo")
483+
@track_ogr_request
484484
def fork_create(self, namespace: Optional[str] = None) -> "GitProject":
485485
if namespace:
486486
self.api.create_fork(
@@ -512,7 +512,7 @@ def change_token(self, new_token: str) -> None:
512512
"Not possible; requires recreation of the httpx client",
513513
)
514514

515-
@track_ogr_request("forgejo")
515+
@track_ogr_request
516516
def get_file_content(
517517
self,
518518
path: str,
@@ -571,7 +571,7 @@ def __get_files(
571571

572572
yield file.path
573573

574-
@track_ogr_request("forgejo")
574+
@track_ogr_request
575575
def get_files(
576576
self,
577577
ref: Optional[str] = None,
@@ -591,7 +591,7 @@ def get_files(
591591

592592
return paths
593593

594-
@track_ogr_request("forgejo")
594+
@track_ogr_request
595595
def get_forks(self) -> Iterable["ForgejoProject"]:
596596
return (
597597
ForgejoProject(
@@ -607,7 +607,7 @@ def get_forks(self) -> Iterable["ForgejoProject"]:
607607
def get_web_url(self) -> str:
608608
return self.forgejo_repo.html_url
609609

610-
@track_ogr_request("forgejo")
610+
@track_ogr_request
611611
def get_sha_from_branch(self, branch: str) -> Optional[str]:
612612
try:
613613
branch_info = self.partial_api(

0 commit comments

Comments
 (0)