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.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik.admin.metrics import metrics
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.geo import GEOIP_READER from authentik.events.geo import GEOIP_READER
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
@ -29,6 +29,7 @@ class Capabilities(models.TextChoices):
CAN_GEO_IP = "can_geo_ip" CAN_GEO_IP = "can_geo_ip"
CAN_IMPERSONATE = "can_impersonate" CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug" CAN_DEBUG = "can_debug"
CAN_TSDB = "can_tsdb"
IS_ENTERPRISE = "is_enterprise" IS_ENTERPRISE = "is_enterprise"
@ -71,6 +72,8 @@ class ConfigView(APIView):
caps.append(Capabilities.CAN_IMPERSONATE) caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG) caps.append(Capabilities.CAN_DEBUG)
if metrics.supported:
caps.append(Capabilities.CAN_TSDB)
if "authentik.enterprise" in settings.INSTALLED_APPS: if "authentik.enterprise" in settings.INSTALLED_APPS:
caps.append(Capabilities.IS_ENTERPRISE) caps.append(Capabilities.IS_ENTERPRISE)
return caps 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.dispatch import receiver
from django.http import HttpRequest from django.http import HttpRequest
from authentik.admin.metrics import Timeseries, metrics
from authentik.core.models import User from authentik.core.models import User
from authentik.core.signals import login_failed, password_changed from authentik.core.signals import login_failed, password_changed
from authentik.events.models import Event, EventAction 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 # Save the login method used
kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD] kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) 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) event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user)
request.session[SESSION_LOGIN_EVENT] = event 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 sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from authentik.admin.metrics import Timeseries, metrics
from authentik.core.models import Application from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict from authentik.events.models import Event, EventAction, cleanse_dict
from authentik.flows.challenge import ( from authentik.flows.challenge import (
@ -291,7 +292,13 @@ class FlowExecutorView(APIView):
with Hub.current.start_span( with Hub.current.start_span(
op="authentik.flow.executor.stage", op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__), 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("Method", "GET")
span.set_data("authentik Stage", self.current_stage_view) span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug) span.set_data("authentik Flow", self.flow.slug)
@ -335,7 +342,13 @@ class FlowExecutorView(APIView):
with Hub.current.start_span( with Hub.current.start_span(
op="authentik.flow.executor.stage", op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__), 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("Method", "POST")
span.set_data("authentik Stage", self.current_stage_view) span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug) span.set_data("authentik Flow", self.flow.slug)
@ -441,6 +454,13 @@ class FlowExecutorView(APIView):
# It's only deleted on a fresh executions # It's only deleted on a fresh executions
# SESSION_KEY_HISTORY, # 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") self._logger.debug("f(exec): cleaning up")
for key in keys_to_delete: for key in keys_to_delete:
if key in self.request.session: 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 model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from authentik.admin.metrics import Timeseries, metrics
from authentik.lib.models import ( from authentik.lib.models import (
CreatedUpdatedModel, CreatedUpdatedModel,
InheritanceAutoManager, InheritanceAutoManager,
@ -93,8 +94,12 @@ class PolicyBinding(SerializerModel):
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if request passes this PolicyBinding, check policy, group or user""" """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: if self.policy:
self.policy: Policy self.policy: Policy
with metrics.observe(
Timeseries.policies_execution_timing, str(self.policy.pk),
):
return self.policy.passes(request) return self.policy.passes(request)
if self.group: if self.group:
return PolicyResult(self.group.is_member(request.user)) return PolicyResult(self.group.is_member(request.user))

View File

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