Skip to content

Commit ccfa52f

Browse files
authored
Merge pull request #106 from lsst-sqre/tickets/DM-49148/app-metrics
DM-49148: First app metric
2 parents 77f1754 + 164fbde commit ccfa52f

File tree

10 files changed

+102
-11
lines changed

10 files changed

+102
-11
lines changed

.github/workflows/docs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ env:
55
# reason to support multiple Python versions, so all actions are run with
66
# this version. Quote the version to avoid interpretation as a floating
77
# point number.
8-
PYTHON_VERSION: "3.12"
8+
PYTHON_VERSION: "3.13"
99

1010
"on":
1111
push:

.github/workflows/periodic-ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ env:
1010
# reason to support multiple Python versions, so all actions are run with
1111
# this version. Quote the version to avoid interpretation as a floating
1212
# point number.
13-
PYTHON_VERSION: "3.12"
13+
PYTHON_VERSION: "3.13"
1414

1515
"on":
1616
schedule:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!-- Delete the sections that don't apply -->
2+
### New features
3+
4+
- Added [Application Metrics](https://safir.lsst.io/user-guide/metrics/index.html) scaffolding, and a single pair of metrics for counting the number of notebook execution tasks that are enqueued.

src/noteburst/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic_settings import BaseSettings
1212
from safir.arq import ArqMode
1313
from safir.logging import LogLevel, Profile
14+
from safir.metrics import MetricsConfiguration, metrics_configuration_factory
1415

1516
__all__ = [
1617
"Config",
@@ -78,6 +79,14 @@ class Config(BaseSettings):
7879
),
7980
] = "/noteburst"
8081

82+
metrics: Annotated[
83+
MetricsConfiguration,
84+
Field(
85+
default_factory=metrics_configuration_factory,
86+
title="Metrics configuration",
87+
),
88+
]
89+
8190
environment_url: Annotated[
8291
HttpUrl,
8392
Field(

src/noteburst/events.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""App metrics events."""
2+
3+
from typing import override
4+
5+
from safir.dependencies.metrics import EventDependency, EventMaker
6+
from safir.metrics import EventManager, EventPayload
7+
8+
__all__ = [
9+
"EnqueueNotebookExecutionFailure",
10+
"EnqueueNotebookExecutionSuccess",
11+
"Events",
12+
"events_dependency",
13+
]
14+
15+
16+
class EnqueueNotebookExecutionSuccess(EventPayload):
17+
"""An nbexec task was successfully enqueued."""
18+
19+
username: str
20+
21+
22+
class EnqueueNotebookExecutionFailure(EventPayload):
23+
"""An nbexec task failed to be enqueued."""
24+
25+
username: str
26+
27+
28+
class Events(EventMaker):
29+
"""A container for app metrics event publishers."""
30+
31+
@override
32+
async def initialize(self, manager: EventManager) -> None:
33+
"""Create event publishers."""
34+
self.enqueue_nbexec_success = await manager.create_publisher(
35+
"enqueue_nbexec_success", EnqueueNotebookExecutionSuccess
36+
)
37+
self.enqueue_nbexec_failure = await manager.create_publisher(
38+
"enqueue_nbexec_failure", EnqueueNotebookExecutionFailure
39+
)
40+
41+
42+
events_dependency = EventDependency(Events())
43+
"""Provides an container that holds event publishers."""

src/noteburst/handlers/v1/handlers.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
from noteburst.exceptions import JobNotFoundError, NoteburstJobError
1818

19+
from ...events import (
20+
EnqueueNotebookExecutionFailure,
21+
EnqueueNotebookExecutionSuccess,
22+
Events,
23+
events_dependency,
24+
)
1925
from .models import NotebookResponse, PostNotebookRequest
2026

2127
v1_router = APIRouter(tags=["v1"], route_class=SlackRouteErrorHandler)
@@ -33,8 +39,10 @@ async def post_nbexec(
3339
*,
3440
request: Request,
3541
response: Response,
42+
user: Annotated[str, Depends(auth_dependency)],
3643
logger: Annotated[structlog.BoundLogger, Depends(auth_logger_dependency)],
3744
arq_queue: Annotated[ArqQueue, Depends(arq_dependency)],
45+
events: Annotated[Events, Depends(events_dependency)],
3846
) -> NotebookResponse:
3947
"""Submits a notebook for execution. The notebook is executed
4048
asynchronously via a pool of JupyterLab (Nublado) instances.
@@ -61,14 +69,24 @@ async def post_nbexec(
6169
and (if available) the notebook (`ipynb`) result. See
6270
`GET /v1/notebooks/{job_id}` for more information.
6371
"""
64-
logger.debug("Enqueing a nbexec task")
65-
job_metadata = await arq_queue.enqueue(
66-
"nbexec",
67-
ipynb=request_data.get_ipynb_as_str(),
68-
kernel_name=request_data.kernel_name,
69-
enable_retry=request_data.enable_retry,
70-
timeout=request_data.timeout,
71-
)
72+
try:
73+
logger.debug("Enqueing a nbexec task")
74+
job_metadata = await arq_queue.enqueue(
75+
"nbexec",
76+
ipynb=request_data.get_ipynb_as_str(),
77+
kernel_name=request_data.kernel_name,
78+
enable_retry=request_data.enable_retry,
79+
timeout=request_data.timeout,
80+
)
81+
await events.enqueue_nbexec_success.publish(
82+
EnqueueNotebookExecutionSuccess(username=user)
83+
)
84+
except:
85+
await events.enqueue_nbexec_failure.publish(
86+
EnqueueNotebookExecutionFailure(username=user)
87+
)
88+
raise
89+
7290
logger.info("Finished enqueing an nbexec task", job_id=job_metadata.id)
7391
response_data = await NotebookResponse.from_job_metadata(
7492
job=job_metadata, request=request

src/noteburst/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from safir.slack.webhook import SlackRouteErrorHandler
2525

2626
from .config import config
27+
from .events import events_dependency
2728
from .handlers.external import external_router
2829
from .handlers.internal import internal_router
2930
from .handlers.v1 import v1_router
@@ -48,6 +49,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
4849
mode=config.arq_mode, redis_settings=config.arq_redis_settings
4950
)
5051

52+
event_manager = config.metrics.make_manager()
53+
await event_manager.initialize()
54+
await events_dependency.initialize(event_manager)
55+
5156
yield
5257

5358
# Shut down event

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131

3232
@pytest_asyncio.fixture
33-
async def app() -> AsyncIterator[FastAPI]:
33+
async def app(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[FastAPI]:
3434
"""Return a configured test application.
3535
3636
Wraps the application in a lifespan manager so that startup and shutdown

tests/handlers/v1_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
import json
66
from pathlib import Path
7+
from typing import cast
78

89
import pytest
910
from httpx import AsyncClient
1011
from safir.arq import MockArqQueue
1112
from safir.dependencies.arq import arq_dependency
13+
from safir.metrics import MockEventPublisher
14+
15+
from noteburst.events import events_dependency
1216

1317

1418
@pytest.fixture
@@ -28,6 +32,8 @@ async def test_post_nbexec(
2832
client: AsyncClient, sample_ipynb: str, sample_ipynb_executed: str
2933
) -> None:
3034
"""Test ``POST /v1/``, sending a notebook to execute."""
35+
events = await events_dependency()
36+
3137
arq_queue = await arq_dependency()
3238
assert isinstance(arq_queue, MockArqQueue)
3339

@@ -45,6 +51,9 @@ async def test_post_nbexec(
4551
assert job_url == response.headers["Location"]
4652
job_id = data["job_id"]
4753

54+
pub = cast(MockEventPublisher, events.enqueue_nbexec_success).published
55+
pub.assert_published_all([{"username": "user"}])
56+
4857
response = await client.get(job_url)
4958
assert response.status_code == 200
5059
data2 = response.json()

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ setenv =
1212
NOTEBURST_REDIS_URL = redis://localhost:6379
1313
NOTEBURST_ARQ_MODE = test
1414
NOTEBURST_WORKER_IDENTITIES_PATH = tests/identities.test.yaml
15+
METRICS_APPLICATION = "noteburst"
16+
METRICS_ENABLED = false
17+
METRICS_MOCK = true
1518
deps =
1619
-r{toxinidir}/requirements/main.txt
1720
-r{toxinidir}/requirements/dev.txt

0 commit comments

Comments
 (0)