initial
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
346c2e2f8f
commit
003608459f
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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,7 +35,8 @@ 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, {})
|
||||
event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,14 +94,18 @@ class PolicyBinding(SerializerModel):
|
|||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check if request passes this PolicyBinding, check policy, group or user"""
|
||||
if self.policy:
|
||||
self.policy: Policy
|
||||
return self.policy.passes(request)
|
||||
if self.group:
|
||||
return PolicyResult(self.group.is_member(request.user))
|
||||
if self.user:
|
||||
return PolicyResult(request.user == self.user)
|
||||
return PolicyResult(False)
|
||||
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))
|
||||
if self.user:
|
||||
return PolicyResult(request.user == self.user)
|
||||
return PolicyResult(False)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue