Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-03-27 00:22:39 +02:00
parent 346c2e2f8f
commit 003608459f
No known key found for this signature in database
6 changed files with 131 additions and 12 deletions

View file

@ -0,0 +1,87 @@
"""Metrics"""
from contextlib import contextmanager
from enum import Enum
from timeit import default_timer
from django.core.cache import cache
from django_redis.client import DefaultClient
from redis import Redis
from redis.exceptions import ResponseError
from structlog.stdlib import BoundLogger, get_logger
class Timeseries(Enum):
"""An enum of all timeseries"""
policies_execution_count = "authentik_policies_execution_count"
policies_execution_timing = "authentik_policies_execution_timing"
flows_execution_count = "authentik_flows_execution_count"
flows_stages_execution_count = "authentik_flows_stages_execution_count"
flows_stages_execution_timing = "authentik_flows_stages_execution_timing"
users_login_count = "authentik_users_login_count"
class MetricsManager:
"""RedisTSDB metrics"""
supported = False
logger: BoundLogger
retention: int
def __init__(self) -> None:
self.supported = self.redis_tsdb_supported()
self.logger = get_logger()
# 1 week in ms
self.retention = 7 * 24 * 60 * 60 * 1000
def redis_tsdb_supported(self):
"""Check if redis has the timeseries module loaded"""
modules = self.get_client().module_list()
supported = any(module[b"name"] == b"timeseries" for module in modules)
return supported
def get_client(self) -> Redis:
cache_client: DefaultClient = cache.client
return cache_client.get_client()
def make_key(self, ts: Timeseries, *key_parts) -> str:
"""Construct a unique key"""
return "_".join([ts.value] + list(key_parts))
@contextmanager
def inc(self, ts: Timeseries, *key_parts, **labels):
"""Increase counter with labels"""
if not self.supported:
yield
return
client = self.get_client()
yield
labels["base_ts"] = ts.value
client.ts().incrby(
self.make_key(ts, *key_parts),
1,
retention_msecs=self.retention,
labels=labels,
)
@contextmanager
def observe(self, ts: Timeseries, *key_parts, **labels):
"""Observe time and save as a sample"""
if not self.supported:
yield
return
client = self.get_client()
start = default_timer()
yield
duration = default_timer() - start
labels["base_ts"] = ts.value
client.ts().add(
self.make_key(ts, *key_parts),
"*",
retention_msecs=self.retention,
value=duration,
labels=labels,
)
metrics = MetricsManager()

View file

@ -16,7 +16,7 @@ from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.admin.metrics import metrics
from authentik.core.api.utils import PassiveSerializer
from authentik.events.geo import GEOIP_READER
from authentik.lib.config import CONFIG
@ -29,6 +29,7 @@ class Capabilities(models.TextChoices):
CAN_GEO_IP = "can_geo_ip"
CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug"
CAN_TSDB = "can_tsdb"
IS_ENTERPRISE = "is_enterprise"
@ -71,6 +72,8 @@ class ConfigView(APIView):
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG)
if metrics.supported:
caps.append(Capabilities.CAN_TSDB)
if "authentik.enterprise" in settings.INSTALLED_APPS:
caps.append(Capabilities.IS_ENTERPRISE)
return caps

View file

@ -6,6 +6,7 @@ from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.admin.metrics import Timeseries, metrics
from authentik.core.models import User
from authentik.core.signals import login_failed, password_changed
from authentik.events.models import Event, EventAction
@ -34,6 +35,7 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
# Save the login method used
kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
with metrics.inc(Timeseries.users_login_count, str(user.pk)):
event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user)
request.session[SESSION_LOGIN_EVENT] = event

View file

@ -22,6 +22,7 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.admin.metrics import Timeseries, metrics
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import (
@ -291,7 +292,13 @@ class FlowExecutorView(APIView):
with Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
) as span, metrics.inc(
Timeseries.flows_stages_execution_count,
str(self.current_stage.pk),
), metrics.observe(
Timeseries.flows_stages_execution_timing,
str(self.current_stage.pk),
):
span.set_data("Method", "GET")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
@ -335,7 +342,13 @@ class FlowExecutorView(APIView):
with Hub.current.start_span(
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
) as span, metrics.inc(
Timeseries.flows_stages_execution_count,
str(self.current_stage.pk),
), metrics.observe(
Timeseries.flows_stages_execution_timing,
str(self.current_stage.pk),
):
span.set_data("Method", "POST")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
@ -441,6 +454,13 @@ class FlowExecutorView(APIView):
# It's only deleted on a fresh executions
# SESSION_KEY_HISTORY,
]
# Increase the flow execution as this function gets called on successful and
# failed flow executions
with metrics.inc(
Timeseries.flows_execution_count,
flow_pk=str(self.flow.pk),
):
pass
self._logger.debug("f(exec): cleaning up")
for key in keys_to_delete:
if key in self.request.session:

View file

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
from authentik.admin.metrics import Timeseries, metrics
from authentik.lib.models import (
CreatedUpdatedModel,
InheritanceAutoManager,
@ -93,8 +94,12 @@ class PolicyBinding(SerializerModel):
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if request passes this PolicyBinding, check policy, group or user"""
with metrics.inc(Timeseries.policies_execution_count, str(self.policy_binding_uuid)):
if self.policy:
self.policy: Policy
with metrics.observe(
Timeseries.policies_execution_timing, str(self.policy.pk),
):
return self.policy.passes(request)
if self.group:
return PolicyResult(self.group.is_member(request.user))

View file

@ -27786,6 +27786,7 @@ components:
- can_geo_ip
- can_impersonate
- can_debug
- can_tsdb
- is_enterprise
type: string
description: |-
@ -27793,6 +27794,7 @@ components:
* `can_geo_ip` - Can Geo Ip
* `can_impersonate` - Can Impersonate
* `can_debug` - Can Debug
* `can_tsdb` - Can Tsdb
* `is_enterprise` - Is Enterprise
CaptchaChallenge:
type: object