initial
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
346c2e2f8f
commit
003608459f
87
authentik/admin/metrics.py
Normal file
87
authentik/admin/metrics.py
Normal 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()
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue