Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a33ba22
docs: add metrics spec for Prometheus phase‑1
Jul 16, 2025
50e708a
chore: add baseline Prometheus scrape config
Jul 16, 2025
13b749e
Add Prometheus metrics, per-asset access logging, and Grafana panels
Aug 6, 2025
e9af280
Review fixes: remove user_id, monitoring profile, env ports, doc, ses…
Aug 6, 2025
b00657f
Expand resource type detection to all router groups
Aug 8, 2025
3ceb6a8
Merge branch 'develop' into feature/metrics-phase1
PGijsbers Aug 9, 2025
3970c41
metrics: versioned /stats/v1/top, robust access logging via parser, t…
Aug 11, 2025
0eac24d
middleware:fix asset path, tests:addition of test access log
Aug 12, 2025
2879ae0
metrics: fix pre-commit issues, correct access_stats router typing, d…
Aug 12, 2025
e5dfe11
main: use importlib.metadata instead of pkg_resources (fix mypy)
Aug 12, 2025
bf11207
metrics: expand docs; add/default Grafana provisioning; stats router …
Aug 12, 2025
8f57972
Merge branch 'develop' into feature/metrics-phase1
PGijsbers Aug 18, 2025
577e8f0
Pre-commit missed by GitHub merge
PGijsbers Aug 18, 2025
d59f3d7
Only keep the identifier of the asset, without path info
PGijsbers Aug 18, 2025
c894717
Do not register the middleware with subapps since it leads to duplicates
PGijsbers Aug 18, 2025
da3b578
type resolution from identifier prefix; grafana: default API metrics …
Aug 19, 2025
094d217
grafana and prometheus bind mounts
Aug 20, 2025
5a52286
Update monitoring information
PGijsbers Aug 21, 2025
914b61a
Update table definition
PGijsbers Aug 21, 2025
c57fc93
Allow arbitrary url_prefix based on the server configuration
PGijsbers Aug 21, 2025
88e23c9
Update path parsing to be more strict and use whitelists
PGijsbers Aug 21, 2025
dea0449
Add constraint to length of identifier
PGijsbers Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ AIOD_NGINX_PORT=80
#DATA STORAGE
DATA_PATH=./data
BACKUP_PATH=./data/backups

#PROMETHEUS and GRAFANA
AIOD_PROMETHEUS_PORT=9090
AIOD_GRAFANA_PORT=3000
30 changes: 30 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,32 @@ services:
es_logstash_setup:
condition: service_completed_successfully

prometheus:
profiles: ["monitoring"]
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
ports:
- "${AIOD_PROMETHEUS_PORT}:9090"
restart: unless-stopped

grafana:
profiles: ["monitoring"]
image: grafana/grafana:latest
container_name: grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- prometheus
# expose later if you need external access
ports:
- "${AIOD_GRAFANA_PORT}:3000"
restart: unless-stopped

taxonomy:
profiles: ["taxonomy"]
container_name: taxonomy
Expand All @@ -247,3 +273,7 @@ services:
depends_on:
sqlserver:
condition: service_healthy

volumes:
prometheus_data:
grafana_data:
7 changes: 7 additions & 0 deletions docs/metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Metrics & Monitoring

* **/metrics** – Prometheus exposition created by prometheus_fastapi_instrumentator.
* **/stats/top/{resource_type}** – JSON list '[asset_id, hits]', success only.
* **AssetAccessLog** – table schema.
* Quickstart – 'docker compose --profile monitoring up -d'.
* Queries – PromQL for per-endpoint, MySQL for per-asset popularity.
Empty file modified logstash/Dockerfile
100644 → 100755
Empty file.
Empty file modified logstash/config/config/jvm.options
100644 → 100755
Empty file.
Empty file modified logstash/config/config/log4j2.file.properties
100644 → 100755
Empty file.
Empty file modified logstash/config/config/log4j2.properties
100644 → 100755
Empty file.
Empty file modified logstash/config/config/logstash-sample.conf
100644 → 100755
Empty file.
Empty file modified logstash/config/config/pipelines.yml
100644 → 100755
Empty file.
Empty file modified logstash/config/config/startup.options
100644 → 100755
Empty file.
Empty file modified logstash/config/pipeline/.gitkeep
100644 → 100755
Empty file.
Empty file modified logstash/config/sql/.gitkeep
100644 → 100755
Empty file.
7 changes: 7 additions & 0 deletions prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
global:
scrape_interval: 1s
scrape_configs:
- job_name: 'aiod_rest_api'
metrics_path: /metrics
static_configs:
- targets: ['app:8000']
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies = [
"mysql-connector-python==9.1.0",
"elasticsearch==8.16.0",
"jinja2==3.1.4",
"prometheus-fastapi-instrumentator==6.1.0",
]
readme = "README.md"

Expand Down
Empty file.
10 changes: 10 additions & 0 deletions src/database/model/access/access_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from datetime import datetime
from sqlmodel import SQLModel, Field


class AssetAccessLog(SQLModel, table=True): # type: ignore[call-arg]
id: int | None = Field(default=None, primary_key=True)
asset_id: str
resource_type: str # “datasets”, “models”, etc.
status: int # HTTP status code
accessed_at: datetime = Field(default_factory=datetime.utcnow, index=True)
24 changes: 15 additions & 9 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
bookmark_router,
asset_router,
)
from setup_logger import setup_logger
from prometheus_fastapi_instrumentator import Instrumentator
from middleware.access_log import AccessLogMiddleware
from versioning import versions, add_version_to_openapi, add_deprecation_and_sunset_middleware


Expand Down Expand Up @@ -139,23 +142,26 @@ def build_app(*, url_prefix: str = "", version: str = "dev"):
version="latest",
**kwargs,
)
add_routes(main_app)
main_app.add_exception_handler(HTTPException, http_exception_handler)
add_version_to_openapi(main_app, root_path=url_prefix)

for version, info in versions.items():
if info.retired:
continue
app = FastAPI(
versioned_apps = [
FastAPI(
title=f"AIoD Metadata Catalogue {version}",
version=f"{version}",
**kwargs,
)
for version, info in versions.items()
if not info.retired
]
for app in [main_app] + versioned_apps:
add_routes(app)
app.add_exception_handler(HTTPException, http_exception_handler)
add_deprecation_and_sunset_middleware(app)
add_version_to_openapi(app, root_path=url_prefix)
main_app.mount(f"/{version}", app)
Instrumentator().instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
app.add_middleware(AccessLogMiddleware)

for app in versioned_apps:
main_app.mount(f"/{app.version}", app)

return main_app


Expand Down
Empty file added src/middleware/__init__.py
Empty file.
33 changes: 33 additions & 0 deletions src/middleware/access_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

from database.session import DbSession
from database.model.access.access_log import AssetAccessLog

from middleware.resource_types import all_resource_types

VALID_TYPES = all_resource_types()


class AccessLogMiddleware(BaseHTTPMiddleware):
"""Write one AssetAccessLog row for /datasets/<id> and /models/<id>."""

async def dispatch(self, request: Request, call_next):
response: Response = await call_next(request)

segments = request.url.path.strip("/").split("/")
if len(segments) >= 2 and segments[0] in VALID_TYPES:
resource_type = segments[0]
asset_id = "/".join(segments[1:])

entry = AssetAccessLog(
asset_id=asset_id,
resource_type=resource_type,
status=response.status_code,
)
with DbSession() as sess:
sess.add(entry)
sess.commit()

return response
21 changes: 21 additions & 0 deletions src/middleware/resource_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Set
from routers import (
resource_routers,
parent_routers,
uploader_routers,
)


def all_resource_types() -> Set[str]:
"""
Gather every plural resource name exposed by *any* router group,
e.g. {'datasets', 'ml_models', 'computational_assets', …}.
Uses getattr guard so it doesn’t crash when a router lacks the attribute.
"""
router_lists = resource_routers.router_list
types: Set[str] = set()
for router in router_lists:
val = getattr(router, "resource_name_plural", None)
if val:
types.add(val)
return types
Loading