events: rework metrics (#4407)

* rework metrics

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* change graphs to be over last week

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix  Apps with most usage card

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-01-11 12:21:07 +01:00 committed by GitHub
parent a35b8f5862
commit 67a6fa6399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 143 additions and 107 deletions

View File

@ -1,4 +1,7 @@
"""authentik administration metrics"""
from datetime import timedelta
from django.db.models.functions import ExtractDay
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField
@ -21,38 +24,44 @@ class CoordinateSerializer(PassiveSerializer):
class LoginMetricsSerializer(PassiveSerializer):
"""Login Metrics per 1h"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
authorizations_per_1h = SerializerMethodField()
logins = SerializerMethodField()
logins_failed = SerializerMethodField()
authorizations = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations_per_1h(self, _):
"""Get successful authorizations per hour for the last 24 hours"""
def get_authorizations(self, _):
"""Get successful authorizations per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.AUTHORIZE_APPLICATION)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)

View File

@ -1,8 +1,10 @@
"""Application API Views"""
from datetime import timedelta
from typing import Optional
from django.core.cache import cache
from django.db.models import QuerySet
from django.db.models.functions import ExtractDay
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
@ -259,10 +261,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Metrics for application logins"""
app = self.get_object()
return Response(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION,
context__authorized_application__pk=app.pk.hex,
)
.get_events_per_hour()
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)

View File

@ -4,6 +4,7 @@ from json import loads
from typing import Any, Optional
from django.contrib.auth import update_session_auth_hash
from django.db.models.functions import ExtractDay
from django.db.models.query import QuerySet
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -199,38 +200,44 @@ class SessionUserSerializer(PassiveSerializer):
class UserMetricsSerializer(PassiveSerializer):
"""User Metrics"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
authorizations_per_1h = SerializerMethodField()
logins = SerializerMethodField()
logins_failed = SerializerMethodField()
authorizations = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN, user__pk=user.pk)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED, context__username=user.username
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
def get_authorizations(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
.get_events_per_hour()
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
)

View File

@ -1,9 +1,11 @@
"""Events API Views"""
from datetime import timedelta
from json import loads
import django_filters
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import ExtractDay
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
@ -178,7 +180,7 @@ class EventViewSet(ModelViewSet):
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)
.filter(**query)
.get_events_per_day()
.get_events_per(timedelta(weeks=4), ExtractDay, 30)
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -11,8 +11,7 @@ from django.conf import settings
from django.db import models
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.db.models.functions.datetime import ExtractDay
from django.db.models.functions import Extract
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.http import HttpRequest
@ -111,48 +110,36 @@ class EventAction(models.TextChoices):
class EventQuerySet(QuerySet):
"""Custom events query set with helper functions"""
def get_events_per_hour(self) -> list[dict[str, int]]:
def get_events_per(
self,
time_since: timedelta,
extract: Extract,
data_points: int,
) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
_now = now()
max_since = timedelta(days=60)
# Allow maximum of 60 days to limit load
if time_since.total_seconds() > max_since.total_seconds():
time_since = max_since
date_from = _now - time_since
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(age=ExpressionWrapper(_now - F("created"), output_field=DurationField()))
.annotate(age_interval=extract("age"))
.values("age_interval")
.annotate(count=Count("pk"))
.order_by("age_hours")
.order_by("age_interval")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
data = Counter({int(d["age_interval"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
interval_timdelta = time_since / data_points
for interval in range(1, -data_points, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
def get_events_per_day(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(weeks=4)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_days=ExtractDay("age"))
.values("age_days")
.annotate(count=Count("pk"))
.order_by("age_days")
)
data = Counter({int(d["age_days"]): d["count"] for d in result})
results = []
_now = now()
for day in range(0, -30, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
"y_cord": data[day * -1],
"x_cord": time.mktime((_now + (interval_timdelta * interval)).timetuple())
* 1000,
"y_cord": data[interval * -1],
}
)
return results
@ -165,13 +152,14 @@ class EventManager(Manager):
"""use custom queryset"""
return EventQuerySet(self.model, using=self._db)
def get_events_per_hour(self) -> list[dict[str, int]]:
def get_events_per(
self,
time_since: timedelta,
extract: Extract,
data_points: int,
) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_hour()
def get_events_per_day(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_day()
return self.get_queryset().get_events_per(time_since, extract, data_points)
class Event(SerializerModel, ExpiringModel):

View File

@ -28957,25 +28957,25 @@ components:
type: object
description: Login Metrics per 1h
properties:
logins_per_1h:
logins:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
logins_failed_per_1h:
logins_failed:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
authorizations_per_1h:
authorizations:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
required:
- authorizations_per_1h
- logins_failed_per_1h
- logins_per_1h
- authorizations
- logins
- logins_failed
LoginSource:
type: object
description: Serializer for Login buttons of sources
@ -37741,25 +37741,25 @@ components:
type: object
description: User Metrics
properties:
logins_per_1h:
logins:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
logins_failed_per_1h:
logins_failed:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
authorizations_per_1h:
authorizations:
type: array
items:
$ref: '#/components/schemas/Coordinate'
readOnly: true
required:
- authorizations_per_1h
- logins_failed_per_1h
- logins_per_1h
- authorizations
- logins
- logins_failed
UserOAuthSourceConnection:
type: object
description: OAuth Source Serializer

View File

@ -180,7 +180,7 @@ export class AdminOverviewPage extends AKElement {
>
<ak-aggregate-card
icon="pf-icon pf-icon-server"
header=${t`Logins and authorizations over the last 24 hours`}
header=${t`Logins and authorizations over the last week (per 8 hours)`}
>
<ak-charts-admin-login-authorization></ak-charts-admin-login-authorization>
</ak-aggregate-card>

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
@ -14,6 +14,13 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} day(s) ago`;
}
getChartData(data: LoginMetrics): ChartData {
return {
datasets: [
@ -25,7 +32,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
fill: "origin",
cubicInterpolationMode: "monotone",
tension: 0.4,
data: data.authorizationsPer1h.map((cord) => {
data: data.authorizations.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,
@ -40,7 +47,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
fill: "origin",
cubicInterpolationMode: "monotone",
tension: 0.4,
data: data.loginsFailedPer1h.map((cord) => {
data: data.loginsFailed.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,
@ -55,7 +62,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
fill: "origin",
cubicInterpolationMode: "monotone",
tension: 0.4,
data: data.loginsPer1h.map((cord) => {
data: data.logins.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
@ -19,6 +19,13 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
});
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} days ago`;
}
getChartData(data: Coordinate[]): ChartData {
return {
datasets: [

View File

@ -225,7 +225,9 @@ export class ApplicationViewPage extends AKElement {
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
>
<div class="pf-c-card__title">${t`Logins over the last 24 hours`}</div>
<div class="pf-c-card__title">
${t`Logins over the last week (per 8 hours)`}
</div>
<div class="pf-c-card__body">
${this.application &&
html` <ak-charts-application-authorize

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
@ -19,6 +19,13 @@ export class UserChart extends AKChart<UserMetrics> {
});
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} days ago`;
}
getChartData(data: UserMetrics): ChartData {
return {
datasets: [
@ -27,7 +34,7 @@ export class UserChart extends AKChart<UserMetrics> {
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data:
data.loginsFailedPer1h?.map((cord) => {
data.loginsFailed?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
@ -39,7 +46,7 @@ export class UserChart extends AKChart<UserMetrics> {
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.loginsPer1h?.map((cord) => {
data.logins?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
@ -51,7 +58,7 @@ export class UserChart extends AKChart<UserMetrics> {
backgroundColor: "rgba(43, 154, 243, .5)",
spanGaps: true,
data:
data.authorizationsPer1h?.map((cord) => {
data.authorizations?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,

View File

@ -276,7 +276,9 @@ export class UserViewPage extends AKElement {
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"
>
<div class="pf-c-card__title">${t`Actions over the last 24 hours`}</div>
<div class="pf-c-card__title">
${t`Actions over the last week (per 8 hours)`}
</div>
<div class="pf-c-card__body">
<ak-charts-user userId=${this.user.pk || 0}> </ak-charts-user>
</div>

View File

@ -40,6 +40,9 @@ export class AggregateCard extends AKElement {
.subtext {
font-size: var(--pf-global--FontSize--sm);
}
.pf-c-card__body {
overflow-x: scroll;
}
`,
]);
}

View File

@ -168,7 +168,7 @@ export abstract class AKChart<T> extends AKElement {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return t`${ago} hours ago`;
return t`${ago} hour(s) ago`;
}
getOptions(): ChartOptions {