Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📈 add active users metric #440

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 1 deletion backend/transcribee_backend/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ def create_user(session: Session, username: str, password: str) -> User:
if existing_user is not None:
raise UserAlreadyExists()
salt, hash = pw_hash(password)
user = User(username=username, password_hash=hash, password_salt=salt)
user = User(
username=username, password_hash=hash, password_salt=salt, last_seen=None
)
session.add(user)
session.commit()
return user
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add User.last_seen

Revision ID: ef78fa0844f4
Revises: 417eece003cb
Create Date: 2024-05-03 16:08:10.602419

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "ef78fa0844f4"
down_revision = "417eece003cb"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True)
)

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("last_seen")

# ### end Alembic commands ###
80 changes: 64 additions & 16 deletions backend/transcribee_backend/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
from transcribee_backend.helpers.time import now_tz_aware
from transcribee_backend.models.document import Document
from transcribee_backend.models.task import Task, TaskAttempt, TaskState
from transcribee_backend.models.user import User
from transcribee_backend.models.user import User, UserToken
from transcribee_backend.models.worker import Worker


class Metric:
class GaugeMetric:
@abstractmethod
def refresh(self, session: Session):
pass


class TasksInState(Metric):
class TasksInState(GaugeMetric):
def __init__(self):
self.collector = Gauge(
"transcribee_tasks", "Number of tasks", ["state", "task_type"]
Expand All @@ -45,7 +45,7 @@ def refresh(self, session: Session):
)


class Workers(Metric):
class Workers(GaugeMetric):
def __init__(self):
self.collector = Gauge("transcribee_workers", "Workers", ["group"])

Expand All @@ -69,25 +69,67 @@ def refresh(self, session: Session):
self.collector.labels(group="alive").set(result)


class Users(Metric):
class Users(GaugeMetric):
def __init__(self):
self.collector = Gauge("transcribee_users", "Registered users")
self.collector = Gauge(
"transcribee_users", "Users at the Transcribee Instance", ["group"]
)

def refresh(self, session: Session):
(result,) = session.query(func.count(User.id)).one()
self.collector.set(result)
self.collector.labels(group="all").set(result)

now = now_tz_aware()
user_timeout_active = now - datetime.timedelta(hours=1)
(result,) = (
session.query(func.count(User.id))
.where(
col(User.last_seen) >= user_timeout_active,
)
.one()
)
self.collector.labels(group="active").set(result)

class Documents(Metric):
now = now_tz_aware()
user_timeout_active = now - datetime.timedelta(hours=1)
(result,) = (
session.query(func.count(User.id))
.where(col(User.last_seen).is_not(None))
.one()
)
self.collector.labels(group="ever_logged_in").set(result)

(result,) = (
session.query(func.count(func.distinct(UserToken.user_id)))
.where(
col(UserToken.valid_until) >= now,
)
.one()
)
self.collector.labels(group="with_token").set(result)


class Documents(GaugeMetric):
def __init__(self):
self.collector = Gauge("transcribe_documents", "Documents")
self.collector = Gauge("transcribe_documents", "Documents", ["group"])

def refresh(self, session: Session):
(result,) = session.query(func.count(Document.id)).one()
self.collector.set(result)
self.collector.labels(group="all").set(result)

now = now_tz_aware()
document_timeout_active = now - datetime.timedelta(hours=1)
(result,) = (
session.query(func.count(func.distinct(Document.id)))
.where(
col(Document.changed_at) >= document_timeout_active,
)
.one()
)
self.collector.labels(group="active").set(result)


class Queue(Metric):
class Queue(GaugeMetric):
def __init__(self):
self.collector = Gauge(
"transcribee_queue_seconds", "Queue length in seconds", ["task_type"]
Expand Down Expand Up @@ -116,19 +158,25 @@ def refresh(self, session: Session):
self.collector.labels(task_type=task_type.value).set(count)


METRIC_CLASSES: List[type[Metric]] = [TasksInState, Workers, Users, Documents, Queue]
METRICS: List[Metric] = []
GAUGE_METRIC_CLASSES: List[type[GaugeMetric]] = [
TasksInState,
Workers,
Users,
Documents,
Queue,
]
GAUGE_METRICS: List[GaugeMetric] = []


def refresh_metrics():
with SessionContextManager(path="repeating_task:refresh_metrics") as session:
for metric in METRICS:
for metric in GAUGE_METRICS:
metric.refresh(session)


def init_metrics():
for klass in METRIC_CLASSES:
METRICS.append(klass())
for klass in GAUGE_METRIC_CLASSES:
GAUGE_METRICS.append(klass())


security = HTTPBasic()
Expand Down
4 changes: 4 additions & 0 deletions backend/transcribee_backend/models/user.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import uuid
from typing import Optional

from pydantic import BaseModel, ConstrainedStr
from sqlmodel import Column, DateTime, Field, Relationship, SQLModel
Expand All @@ -18,6 +19,9 @@ class User(UserBase, table=True):
)
password_hash: bytes
password_salt: bytes
last_seen: Optional[datetime.datetime] = Field(
sa_column=Column(DateTime(timezone=True), nullable=True)
)


class CreateUser(UserBase):
Expand Down
5 changes: 5 additions & 0 deletions backend/transcribee_backend/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ def logout(
@user_router.get("/me/")
def read_user(
token: UserToken = Depends(get_user_token),
session: Session = Depends(get_session),
) -> UserBase:
token.user.last_seen = now_tz_aware()
session.add(token.user)
session.commit()

return UserBase(username=token.user.username)


Expand Down
Loading