Skip to content

Commit

Permalink
Merge pull request #1858 from DSD-DBS/store-feedback
Browse files Browse the repository at this point in the history
feat(monitoring): Add Grafana dashboard for feedback
  • Loading branch information
MoritzWeber0 authored Oct 1, 2024
2 parents 4dde05b + 1ffe95c commit 3a6fe86
Show file tree
Hide file tree
Showing 16 changed files with 719 additions and 20 deletions.
2 changes: 2 additions & 0 deletions backend/capellacollab/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from capellacollab.config import config
from capellacollab.core import logging as core_logging
from capellacollab.core.database import engine, migration
from capellacollab.feedback import metrics as feedback_metrics
from capellacollab.routes import router
from capellacollab.sessions import idletimeout, operators

Expand Down Expand Up @@ -73,6 +74,7 @@ async def shutdown():
idletimeout.terminate_idle_sessions_in_background,
sessions_metrics.register,
t4c_metrics.register,
feedback_metrics.register,
pipeline_runs_interface.schedule_refresh_and_trigger_pipeline_jobs,
],
middleware=[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add feedback table
Revision ID: 7cf3357ddd7b
Revises: abddaf015966
Create Date: 2024-09-30 19:47:36.253187
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "7cf3357ddd7b"
down_revision = "abddaf015966"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"feedback",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"rating",
sa.Enum("BAD", "OKAY", "GOOD", name="feedbackrating"),
nullable=False,
),
sa.Column("feedback_text", sa.String(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("trigger", sa.String(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_feedback_id"), "feedback", ["id"], unique=False)
1 change: 1 addition & 0 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# These import statements of the models are required and should not be removed! (SQLAlchemy will not load the models otherwise)

import capellacollab.events.models
import capellacollab.feedback.models
import capellacollab.notices.models
import capellacollab.projects.models
import capellacollab.projects.toolmodels.backups.models
Expand Down
57 changes: 57 additions & 0 deletions backend/capellacollab/feedback/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import datetime

import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.users import models as users_models

from . import models


def count_feedback_by_rating(
db: orm.Session,
) -> dict[models.FeedbackRating, int]:
return {
value[0]: value[1]
for value in db.execute(
sa.select(
models.DatabaseFeedback.rating, sa.func.count()
).group_by(models.DatabaseFeedback.rating)
).all()
}


def save_feedback(
db: orm.Session,
rating: models.FeedbackRating,
user: users_models.DatabaseUser | None,
feedback_text: str | None,
created_at: datetime.datetime,
trigger: str | None,
) -> models.DatabaseFeedback:
model = models.DatabaseFeedback(
rating=rating,
user=user,
feedback_text=feedback_text,
created_at=created_at,
trigger=trigger,
)

db.add(model)
db.commit()
db.refresh(model)
return model


def anonymize_feedback_of_user(
db: orm.Session, user: users_models.DatabaseUser
):
db.execute(
sa.update(models.DatabaseFeedback)
.where(models.DatabaseFeedback.user_id == user.id)
.values(user_id=None)
)
db.commit()
33 changes: 33 additions & 0 deletions backend/capellacollab/feedback/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import typing as t

import prometheus_client
import prometheus_client.core
from prometheus_client import registry as prometheus_registry

from capellacollab.core import database

from . import crud, models


class FeedbackCollector(prometheus_registry.Collector):
def collect(self) -> t.Iterable[prometheus_client.core.Metric]:
metric = prometheus_client.core.GaugeMetricFamily(
"feedback_count",
"Submitted feedback",
labels=["rating"],
)

with database.SessionLocal() as db:
feedback = crud.count_feedback_by_rating(db)

for rating in models.FeedbackRating:
metric.add_metric([str(rating.value)], feedback.get(rating, 0))

yield metric


def register() -> None:
prometheus_client.REGISTRY.register(FeedbackCollector())
27 changes: 27 additions & 0 deletions backend/capellacollab/feedback/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
import enum

import pydantic
import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.core import database
from capellacollab.core import models as core_models
from capellacollab.core import pydantic as core_pydantic
from capellacollab.sessions import models as sessions_models
from capellacollab.tools import models as tools_models
from capellacollab.users import models as users_models


class FeedbackRating(str, enum.Enum):
Expand All @@ -18,6 +22,29 @@ class FeedbackRating(str, enum.Enum):
GOOD = "good"


class DatabaseFeedback(database.Base):
__tablename__ = "feedback"

id: orm.Mapped[int] = orm.mapped_column(
init=False, primary_key=True, index=True
)
rating: orm.Mapped[FeedbackRating]
feedback_text: orm.Mapped[str | None]

user_id: orm.Mapped[int | None] = orm.mapped_column(
sa.ForeignKey("users.id"),
init=False,
)
user: orm.Mapped[users_models.DatabaseUser | None] = orm.relationship(
foreign_keys=[user_id],
cascade="all, delete-orphan",
single_parent=True,
)

trigger: orm.Mapped[str | None]
created_at: orm.Mapped[datetime.datetime]


class AnonymizedSession(core_pydantic.BaseModel):
id: str
type: sessions_models.SessionType
Expand Down
20 changes: 18 additions & 2 deletions backend/capellacollab/feedback/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0


import datetime
import logging
import typing as t

Expand All @@ -17,7 +18,7 @@
from capellacollab.users import injectables as user_injectables
from capellacollab.users import models as users_models

from . import models, util
from . import crud, models, util

router = fastapi.APIRouter()

Expand Down Expand Up @@ -57,7 +58,22 @@ def submit_feedback(
logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger),
):
util.check_if_feedback_is_allowed(db)
feedback_user = user if feedback.share_contact else None

crud.save_feedback(
db,
feedback.rating,
feedback_user,
feedback.feedback_text,
datetime.datetime.now(),
feedback.trigger,
)

background_tasks.add_task(
util.send_feedback_email, db, feedback, user, user_agent, logger
util.send_feedback_email,
db,
feedback,
feedback_user,
user_agent,
logger,
)
6 changes: 2 additions & 4 deletions backend/capellacollab/feedback/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,14 @@ def format_email(
def send_feedback_email(
db: orm.Session,
feedback: models.Feedback,
user: users_models.DatabaseUser,
user: users_models.DatabaseUser | None,
user_agent: str | None,
logger: logging.LoggerAdapter,
):
check_if_feedback_is_allowed(db)
assert config.smtp # Already checked in previous function
cfg = config_core.get_global_configuration(db)

email_text = format_email(
feedback, user if feedback.share_contact else None, user_agent
)
email_text = format_email(feedback, user, user_agent)

email_send.send_email(cfg.feedback.recipients, email_text, logger)
5 changes: 3 additions & 2 deletions backend/capellacollab/settings/modelsources/t4c/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import prometheus_client
import prometheus_client.core
from prometheus_client import registry as prometheus_registry

from capellacollab.core import database

from . import crud, interface


class UsedT4CLicensesCollector:
class UsedT4CLicensesCollector(prometheus_registry.Collector):
def collect(self) -> t.Iterable[prometheus_client.core.Metric]:
metric = prometheus_client.core.GaugeMetricFamily(
"used_t4c_licenses",
Expand All @@ -36,7 +37,7 @@ def collect(self) -> t.Iterable[prometheus_client.core.Metric]:
yield metric


class TotalT4CLicensesCollector:
class TotalT4CLicensesCollector(prometheus_registry.Collector):
def collect(self) -> t.Iterable[prometheus_client.core.Metric]:
metric = prometheus_client.core.GaugeMetricFamily(
"total_t4c_licenses",
Expand Down
2 changes: 2 additions & 0 deletions backend/capellacollab/users/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.events import crud as events_crud
from capellacollab.events import models as events_models
from capellacollab.feedback import crud as feedback_crud
from capellacollab.projects import crud as projects_crud
from capellacollab.projects import models as projects_models
from capellacollab.projects.users import crud as projects_users_crud
Expand Down Expand Up @@ -163,6 +164,7 @@ def delete_user(
projects_users_crud.delete_projects_for_user(db, user.id)
events_crud.delete_all_events_user_involved_in(db, user.id)
workspaces_util.delete_all_workspaces_of_user(db, user)
feedback_crud.anonymize_feedback_of_user(db, user)
crud.delete_user(db, user)


Expand Down
3 changes: 3 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ def commit(*args, **kwargs):
session.flush()
session.expire_all()

monkeypatch.setattr(database, "SessionLocal", lambda: session)
monkeypatch.setattr(session, "commit", commit)

yield session

del app.dependency_overrides[database.get_db]


@pytest.fixture(name="client")
def fixture_client() -> testclient.TestClient:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,7 @@ def get_mock_existing_pipeline_run() -> runs_models.DatabasePipelineRun:

yield

app.dependency_overrides.pop(
runs_injectables.get_existing_pipeline_run, None
)
del app.dependency_overrides[runs_injectables.get_existing_pipeline_run]


@mock.patch("capellacollab.core.logging.loki.fetch_logs_from_loki")
Expand Down
Loading

0 comments on commit 3a6fe86

Please sign in to comment.