Skip to content

Commit 47b0b11

Browse files
committed
Add api count metrics, add get_user_id method
1 parent ec8bd64 commit 47b0b11

File tree

8 files changed

+65
-25
lines changed

8 files changed

+65
-25
lines changed

gen3workflow/app.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
from fastapi import FastAPI
2+
from fastapi.security import HTTPAuthorizationCredentials
23
import httpx
34
from importlib.metadata import version
45
import os
6+
import time
57

68
from cdislogging import get_logger
79
from gen3authz.client.arborist.async_client import ArboristClient
8-
10+
from fastapi import Request, HTTPException
911
from gen3workflow import logger
1012
from gen3workflow.config import config
1113
from gen3workflow.metrics import Metrics
1214
from gen3workflow.routes.ga4gh_tes import router as ga4gh_tes_router
1315
from gen3workflow.routes.s3 import router as s3_router
1416
from gen3workflow.routes.storage import router as storage_router
1517
from gen3workflow.routes.system import router as system_router
18+
from gen3workflow.auth import Auth
1619

1720

1821
def get_app(httpx_client=None) -> FastAPI:
@@ -65,6 +68,39 @@ def get_app(httpx_client=None) -> FastAPI:
6568
if app.metrics.enabled:
6669
app.include_router("/metrics", app.metrics.get_asgi_app())
6770

71+
@app.middleware("http")
72+
async def middleware_log_response_and_api_metric(
73+
request: Request, call_next
74+
) -> None:
75+
"""
76+
This FastAPI middleware effectively allows pre and post logic to a request.
77+
78+
We are using this to log the response consistently across defined endpoints (including execution time).
79+
80+
Args:
81+
request (Request): the incoming HTTP request
82+
call_next (Callable): function to call (this is handled by FastAPI's middleware support)
83+
"""
84+
start_time = time.perf_counter()
85+
response = await call_next(request)
86+
response_time_seconds = time.perf_counter() - start_time
87+
88+
path = request.url.path
89+
method = request.method
90+
if path not in config["ENDPOINTS_WITH_METRICS"]:
91+
return response
92+
93+
# TODO: Add user_id to this metric
94+
metrics = app.metrics
95+
metrics.add_create_task_api_interaction(
96+
method=method,
97+
path=path,
98+
response_time_seconds=response_time_seconds,
99+
status_code=response.status_code,
100+
)
101+
102+
return response
103+
68104
return app
69105

70106

gen3workflow/auth.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Union
12
from authutils.token.fastapi import access_token
23
from fastapi import HTTPException, Security
34
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -65,6 +66,13 @@ async def get_token_claims(self) -> dict:
6566

6667
return token_claims
6768

69+
async def get_user_id(self) -> Union[str, None]:
70+
try:
71+
token_claims = await self.get_token_claims()
72+
except Exception:
73+
return None
74+
return token_claims.get("sub")
75+
6876
async def authorize(
6977
self,
7078
method: str,

gen3workflow/config-default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ TASK_IMAGE_WHITELIST: []
6161

6262
ENABLE_PROMETHEUS_METRICS: false
6363
PROMETHEUS_MULTIPROC_DIR: /var/tmp/prometheus_metrics
64+
ENDPOINTS_WITH_METRICS: [ /ga4gh/tes/v1/tasks ]

gen3workflow/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def validate_top_level_configs(self) -> None:
6363
"TES_SERVER_URL": {"type": "string"},
6464
"ENABLE_PROMETHEUS_METRICS": {"type": "boolean"},
6565
"PROMETHEUS_MULTIPROC_DIR": {"type": "string"},
66+
"ENDPOINTS_WITH_METRICS": {
67+
"type": "array",
68+
"items": {"type": "string"},
69+
},
6670
},
6771
}
6872
validate(instance=self, schema=schema)

gen3workflow/metrics.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ def __init__(self, prometheus_dir: str, enabled: bool = True) -> None:
1010
super().__init__(
1111
prometheus_dir=config["PROMETHEUS_MULTIPROC_DIR"], enabled=enabled
1212
)
13+
14+
def add_create_task_api_interaction(
15+
self,
16+
**kwargs: Dict[str, Any],
17+
) -> None:
18+
"""
19+
Add a metric for create_task API interactions
20+
"""
21+
self.increment_counter(name="create_task_api_interaction", labels=kwargs)

gen3workflow/routes/ga4gh_tes.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ async def get_request_body(request: Request):
3232

3333
@router.get("/service-info", status_code=HTTP_200_OK)
3434
async def service_info(request: Request, auth=Depends(Auth)) -> dict:
35-
try:
36-
token_claims = await auth.get_token_claims()
37-
except Exception:
38-
token_claims = {}
39-
user_id = token_claims.get("sub")
35+
user_id = await auth.get_user_id()
4036
logger.info(f"User '{user_id}' getting TES service info")
4137

4238
url = f"{config['TES_SERVER_URL']}/service-info"
@@ -164,11 +160,7 @@ def apply_view_to_task(view: str, task: dict) -> dict:
164160

165161
@router.get("/tasks", status_code=HTTP_200_OK)
166162
async def list_tasks(request: Request, auth=Depends(Auth)) -> dict:
167-
try:
168-
token_claims = await auth.get_token_claims()
169-
except Exception:
170-
token_claims = {}
171-
user_id = token_claims.get("sub")
163+
user_id = await auth.get_user_id()
172164
logger.info(f"User '{user_id}' listing TES tasks")
173165

174166
supported_params = {
@@ -231,11 +223,7 @@ async def list_tasks(request: Request, auth=Depends(Auth)) -> dict:
231223

232224
@router.get("/tasks/{task_id}", status_code=HTTP_200_OK)
233225
async def get_task(request: Request, task_id: str, auth=Depends(Auth)) -> dict:
234-
try:
235-
token_claims = await auth.get_token_claims()
236-
except Exception:
237-
token_claims = {}
238-
user_id = token_claims.get("sub")
226+
user_id = await auth.get_user_id()
239227
logger.info(f"User '{user_id}' getting TES task '{task_id}'")
240228

241229
supported_params = {"view"}
@@ -268,11 +256,7 @@ async def get_task(request: Request, task_id: str, auth=Depends(Auth)) -> dict:
268256

269257
@router.post("/tasks/{task_id}:cancel", status_code=HTTP_200_OK)
270258
async def cancel_task(request: Request, task_id: str, auth=Depends(Auth)) -> dict:
271-
try:
272-
token_claims = await auth.get_token_claims()
273-
except Exception:
274-
token_claims = {}
275-
user_id = token_claims.get("sub")
259+
user_id = await auth.get_user_id()
276260
logger.info(f"User '{user_id}' canceling TES task '{task_id}'")
277261

278262
# check if this user has access to delete this task

gen3workflow/routes/s3.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ async def s3_endpoint(path: str, request: Request):
8686
await auth.authorize("create", ["/services/workflow/gen3-workflow/tasks"])
8787

8888
# get the name of the user's bucket and ensure the user is making a call to their own bucket
89-
token_claims = await auth.get_token_claims()
90-
user_id = token_claims.get("sub")
89+
user_id = await auth.get_user_id()
9190
logger.info(f"Incoming S3 request from user '{user_id}': '{request.method} {path}'")
9291
user_bucket = aws_utils.get_safe_name_from_hostname(user_id)
9392
request_bucket = path.split("?")[0].split("/")[0]

gen3workflow/routes/storage.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
@router.get("/info", status_code=HTTP_200_OK)
1212
async def get_storage_info(request: Request, auth=Depends(Auth)) -> dict:
13-
token_claims = await auth.get_token_claims()
14-
user_id = token_claims.get("sub")
13+
user_id = await auth.get_user_id()
1514
logger.info(f"User '{user_id}' getting their own storage info")
1615
bucket_name, bucket_prefix, bucket_region = aws_utils.create_user_bucket(user_id)
1716
return {

0 commit comments

Comments
 (0)