diff --git a/Pipfile.lock b/Pipfile.lock index 226dd0c02..6c68db60b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -354,11 +354,11 @@ }, "django-guardian": { "hashes": [ - "sha256:0e70706c6cda88ddaf8849bddb525b8df49de05ba0798d4b3506049f0d95cbc8", - "sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b" + "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697", + "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "django-model-utils": { "hashes": [ @@ -616,11 +616,11 @@ }, "ldap3": { "hashes": [ + "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59", "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57", "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c", - "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59", - "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", - "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056" + "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056", + "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91" ], "index": "pypi", "version": "==2.9" @@ -835,37 +835,37 @@ }, "pyasn1": { "hashes": [ - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2" ], "version": "==0.4.8" }, "pyasn1-modules": { "hashes": [ - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd" + "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", + "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405", + "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", + "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", + "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", + "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" ], "version": "==0.2.8" }, @@ -1027,8 +1027,8 @@ "requests-oauthlib": { "hashes": [ "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc", + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d" ], "index": "pypi", "version": "==1.3.0" diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py index cfce015ee..dd6932cc2 100644 --- a/authentik/admin/api/tasks.py +++ b/authentik/admin/api/tasks.py @@ -22,7 +22,7 @@ class TaskSerializer(PassiveSerializer): task_name = CharField() task_description = CharField() - task_finish_timestamp = DateTimeField(source="finish_timestamp") + task_finish_timestamp = DateTimeField(source="finish_time") status = ChoiceField( source="result.status.name", diff --git a/authentik/admin/api/workers.py b/authentik/admin/api/workers.py index 4433fecd9..ff9b7c5e2 100644 --- a/authentik/admin/api/workers.py +++ b/authentik/admin/api/workers.py @@ -1,5 +1,6 @@ """authentik administration overview""" from drf_spectacular.utils import extend_schema, inline_serializer +from prometheus_client import Gauge from rest_framework.fields import IntegerField from rest_framework.permissions import IsAdminUser from rest_framework.request import Request @@ -8,6 +9,8 @@ from rest_framework.views import APIView from authentik.root.celery import CELERY_APP +GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers") + class WorkerView(APIView): """Get currently connected worker count.""" @@ -19,4 +22,5 @@ class WorkerView(APIView): ) def get(self, request: Request) -> Response: """Get currently connected worker count.""" - return Response({"count": len(CELERY_APP.control.ping(timeout=0.5))}) + count = len(CELERY_APP.control.ping(timeout=0.5)) + return Response({"count": count}) diff --git a/authentik/admin/tasks.py b/authentik/admin/tasks.py index dcb52601a..26fe1bbe3 100644 --- a/authentik/admin/tasks.py +++ b/authentik/admin/tasks.py @@ -1,13 +1,15 @@ """authentik admin tasks""" import re +from os import environ from django.core.cache import cache from django.core.validators import URLValidator from packaging.version import parse +from prometheus_client import Info from requests import RequestException, get from structlog.stdlib import get_logger -from authentik import __version__ +from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.events.models import Event, EventAction from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.root.celery import CELERY_APP @@ -17,6 +19,18 @@ VERSION_CACHE_KEY = "authentik_latest_version" VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours # Chop of the first ^ because we want to search the entire string URL_FINDER = URLValidator.regex.pattern[1:] +PROM_INFO = Info("authentik_version", "Currently running authentik version") + + +def _set_prom_info(): + """Set prometheus info for version""" + PROM_INFO.info( + { + "version": __version__, + "latest": cache.get(VERSION_CACHE_KEY, ""), + "build_hash": environ.get(ENV_GIT_HASH_KEY, ""), + } + ) @CELERY_APP.task(bind=True, base=MonitoredTask) @@ -36,6 +50,7 @@ def update_latest_version(self: MonitoredTask): TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"] ) ) + _set_prom_info() # Check if upstream version is newer than what we're running, # and if no event exists yet, create one. local_version = parse(__version__) @@ -53,3 +68,6 @@ def update_latest_version(self: MonitoredTask): except (RequestException, IndexError) as exc: cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) + + +_set_prom_info() diff --git a/authentik/core/apps.py b/authentik/core/apps.py index fe5660cca..ef222669f 100644 --- a/authentik/core/apps.py +++ b/authentik/core/apps.py @@ -2,6 +2,10 @@ from importlib import import_module from django.apps import AppConfig +from django.db import ProgrammingError + +from authentik.core.signals import GAUGE_MODELS +from authentik.lib.utils.reflection import get_apps class AuthentikCoreConfig(AppConfig): @@ -15,3 +19,12 @@ class AuthentikCoreConfig(AppConfig): def ready(self): import_module("authentik.core.signals") import_module("authentik.core.managed") + try: + for app in get_apps(): + for model in app.get_models(): + GAUGE_MODELS.labels( + model_name=model._meta.model_name, + app=model._meta.app_label, + ).set(model.objects.count()) + except ProgrammingError: + pass diff --git a/authentik/core/signals.py b/authentik/core/signals.py index ec7675fe2..089ed7bb1 100644 --- a/authentik/core/signals.py +++ b/authentik/core/signals.py @@ -1,20 +1,31 @@ """authentik core signals""" from django.core.cache import cache from django.core.signals import Signal +from django.db.models import Model from django.db.models.signals import post_save from django.dispatch import receiver +from prometheus_client import Gauge # Arguments: user: User, password: str password_changed = Signal() +GAUGE_MODELS = Gauge( + "authentik_models", "Count of various objects", ["model_name", "app"] +) + @receiver(post_save) # pylint: disable=unused-argument -def post_save_application(sender, instance, created: bool, **_): +def post_save_application(sender: type[Model], instance, created: bool, **_): """Clear user's application cache upon application creation""" from authentik.core.api.applications import user_app_cache_key from authentik.core.models import Application + GAUGE_MODELS.labels( + model_name=sender._meta.model_name, + app=sender._meta.app_label, + ).set(sender.objects.count()) + if sender != Application: return if not created: # pragma: no cover diff --git a/authentik/events/apps.py b/authentik/events/apps.py index ad9e7d205..f0eb77c9c 100644 --- a/authentik/events/apps.py +++ b/authentik/events/apps.py @@ -1,7 +1,10 @@ """authentik events app""" +from datetime import timedelta from importlib import import_module from django.apps import AppConfig +from django.db import ProgrammingError +from django.utils.timezone import datetime class AuthentikEventsConfig(AppConfig): @@ -13,3 +16,12 @@ class AuthentikEventsConfig(AppConfig): def ready(self): import_module("authentik.events.signals") + try: + from authentik.events.models import Event + + date_from = datetime.now() - timedelta(days=1) + + for event in Event.objects.filter(created__gte=date_from): + event._set_prom_metrics() + except ProgrammingError: + pass diff --git a/authentik/events/models.py b/authentik/events/models.py index cc41586af..55bb64d98 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -11,6 +11,7 @@ from django.http import HttpRequest from django.utils.timezone import now from django.utils.translation import gettext as _ from geoip2.errors import GeoIP2Error +from prometheus_client import Gauge from requests import RequestException, post from structlog.stdlib import get_logger @@ -28,6 +29,11 @@ from authentik.policies.models import PolicyBindingModel from authentik.stages.email.utils import TemplateEmailMessage LOGGER = get_logger("authentik.events") +GAUGE_EVENTS = Gauge( + "authentik_events", + "Events in authentik", + ["action", "user_username", "app", "client_ip"], +) def default_event_duration(): @@ -169,6 +175,14 @@ class Event(ExpiringModel): except GeoIP2Error as exc: LOGGER.warning("Failed to add geoIP Data to event", exc=exc) + def _set_prom_metrics(self): + GAUGE_EVENTS.labels( + action=self.action, + user_username=self.user.get("username"), + app=self.app, + client_ip=self.client_ip, + ).set(self.created.timestamp()) + def save(self, *args, **kwargs): if self._state.adding: LOGGER.debug( @@ -178,7 +192,8 @@ class Event(ExpiringModel): client_ip=self.client_ip, user=self.user, ) - return super().save(*args, **kwargs) + super().save(*args, **kwargs) + self._set_prom_metrics() @property def summary(self) -> str: diff --git a/authentik/events/monitored_tasks.py b/authentik/events/monitored_tasks.py index 456eb2468..d3a269aed 100644 --- a/authentik/events/monitored_tasks.py +++ b/authentik/events/monitored_tasks.py @@ -2,14 +2,22 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum +from timeit import default_timer from traceback import format_tb from typing import Any, Optional from celery import Task from django.core.cache import cache +from prometheus_client import Gauge from authentik.events.models import Event, EventAction +GAUGE_TASKS = Gauge( + "authentik_system_tasks", + "System tasks and their status", + ["task_name", "task_uid", "status"], +) + class TaskResultStatus(Enum): """Possible states of tasks""" @@ -43,7 +51,9 @@ class TaskInfo: """Info about a task run""" task_name: str - finish_timestamp: datetime + start_timestamp: float + finish_timestamp: float + finish_time: datetime result: TaskResult @@ -73,12 +83,25 @@ class TaskInfo: """Delete task info from cache""" return cache.delete(f"task_{self.task_name}") + def set_prom_metrics(self): + """Update prometheus metrics""" + start = default_timer() + if hasattr(self, "start_timestamp"): + start = self.start_timestamp + duration = max(self.finish_timestamp - start, 0) + GAUGE_TASKS.labels( + task_name=self.task_name, + task_uid=self.result.uid or "", + status=self.result.status, + ).set(duration) + def save(self, timeout_hours=6): """Save task into cache""" key = f"task_{self.task_name}" if self.result.uid: key += f"_{self.result.uid}" self.task_name += f"_{self.result.uid}" + self.set_prom_metrics() cache.set(key, self, timeout=timeout_hours * 60 * 60) @@ -98,6 +121,7 @@ class MonitoredTask(Task): self._uid = None self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[]) self.result_timeout_hours = 6 + self.start = default_timer() def set_uid(self, uid: str): """Set UID, so in the case of an unexpected error its saved correctly""" @@ -117,7 +141,9 @@ class MonitoredTask(Task): TaskInfo( task_name=self.__name__, task_description=self.__doc__, - finish_timestamp=datetime.now(), + start_timestamp=self.start, + finish_timestamp=default_timer(), + finish_time=datetime.now(), result=self._result, task_call_module=self.__module__, task_call_func=self.__name__, @@ -133,7 +159,9 @@ class MonitoredTask(Task): TaskInfo( task_name=self.__name__, task_description=self.__doc__, - finish_timestamp=datetime.now(), + start_timestamp=self.start, + finish_timestamp=default_timer(), + finish_time=datetime.now(), result=self._result, task_call_module=self.__module__, task_call_func=self.__name__, @@ -151,3 +179,7 @@ class MonitoredTask(Task): def run(self, *args, **kwargs): raise NotImplementedError + + +for task in TaskInfo.all().values(): + task.set_prom_metrics() diff --git a/authentik/flows/planner.py b/authentik/flows/planner.py index f66f02dc6..718f64cfa 100644 --- a/authentik/flows/planner.py +++ b/authentik/flows/planner.py @@ -4,6 +4,7 @@ from typing import Any, Optional from django.core.cache import cache from django.http import HttpRequest +from prometheus_client import Histogram from sentry_sdk.hub import Hub from sentry_sdk.tracing import Span from structlog.stdlib import BoundLogger, get_logger @@ -14,6 +15,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.policies.engine import PolicyEngine +from authentik.root.monitoring import UpdatingGauge LOGGER = get_logger() PLAN_CONTEXT_PENDING_USER = "pending_user" @@ -21,6 +23,16 @@ PLAN_CONTEXT_SSO = "is_sso" PLAN_CONTEXT_REDIRECT = "redirect" PLAN_CONTEXT_APPLICATION = "application" PLAN_CONTEXT_SOURCE = "source" +GAUGE_FLOWS_CACHED = UpdatingGauge( + "authentik_flows_cached", + "Cached flows", + update_func=lambda: len(cache.keys("flow_*")), +) +HIST_FLOWS_PLAN_TIME = Histogram( + "authentik_flows_plan_time", + "Duration to build a plan for a flow", + ["flow_slug"], +) def cache_key(flow: Flow, user: Optional[User] = None) -> str: @@ -146,6 +158,7 @@ class FlowPlanner: ) plan = self._build_plan(user, request, default_context) cache.set(cache_key(self.flow, user), plan) + GAUGE_FLOWS_CACHED.update() if not plan.stages and not self.allow_empty_flows: raise EmptyFlowException() return plan @@ -158,7 +171,9 @@ class FlowPlanner: ) -> FlowPlan: """Build flow plan by checking each stage in their respective order and checking the applied policies""" - with Hub.current.start_span(op="flow.planner.build_plan") as span: + with Hub.current.start_span( + op="flow.planner.build_plan" + ) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time(): span: Span span.set_data("flow", self.flow) span.set_data("user", user) @@ -202,6 +217,7 @@ class FlowPlanner: marker = ReevaluateMarker(binding=binding, user=user) if stage: plan.append(stage, marker) + HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug) self._logger.debug( "f(plan): finished building", ) diff --git a/authentik/outposts/channels.py b/authentik/outposts/channels.py index d0dad47b8..3e46dceed 100644 --- a/authentik/outposts/channels.py +++ b/authentik/outposts/channels.py @@ -8,11 +8,21 @@ from channels.exceptions import DenyConnection from dacite import from_dict from dacite.data import Data from guardian.shortcuts import get_objects_for_user +from prometheus_client import Gauge from structlog.stdlib import get_logger from authentik.core.channels import AuthJsonConsumer from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState +GAUGE_OUTPOSTS_CONNECTED = Gauge( + "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"] +) +GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( + "authentik_outposts_last_update", + "Last update from any outpost", + ["outpost", "uid", "version"], +) + LOGGER = get_logger() @@ -44,6 +54,8 @@ class OutpostConsumer(AuthJsonConsumer): last_uid: Optional[str] = None + first_msg = False + def connect(self): super().connect() uuid = self.scope["url_route"]["kwargs"]["pk"] @@ -68,6 +80,10 @@ class OutpostConsumer(AuthJsonConsumer): if self.channel_name in state.channel_ids: state.channel_ids.remove(self.channel_name) state.save() + GAUGE_OUTPOSTS_CONNECTED.labels( + outpost=self.outpost.name, + uid=self.last_uid, + ).dec() LOGGER.debug( "removed outpost instance from cache", outpost=self.outpost, @@ -78,15 +94,29 @@ class OutpostConsumer(AuthJsonConsumer): msg = from_dict(WebsocketMessage, content) uid = msg.args.get("uuid", self.channel_name) self.last_uid = uid + state = OutpostState.for_instance_uid(self.outpost, uid) if self.channel_name not in state.channel_ids: state.channel_ids.append(self.channel_name) state.last_seen = datetime.now() + + if not self.first_msg: + GAUGE_OUTPOSTS_CONNECTED.labels( + outpost=self.outpost.name, + uid=self.last_uid, + ).inc() + self.first_msg = True + if msg.instruction == WebsocketMessageInstruction.HELLO: state.version = msg.args.get("version", None) state.build_hash = msg.args.get("buildHash", "") elif msg.instruction == WebsocketMessageInstruction.ACK: return + GAUGE_OUTPOSTS_LAST_UPDATE.labels( + outpost=self.outpost.name, + uid=self.last_uid or "", + version=state.version or "", + ).set_to_current_time() state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK) diff --git a/authentik/policies/engine.py b/authentik/policies/engine.py index b8ac67fa2..99f27b1d5 100644 --- a/authentik/policies/engine.py +++ b/authentik/policies/engine.py @@ -5,6 +5,7 @@ from typing import Iterator, Optional from django.core.cache import cache from django.http import HttpRequest +from prometheus_client import Histogram from sentry_sdk.hub import Hub from sentry_sdk.tracing import Span from structlog.stdlib import BoundLogger, get_logger @@ -18,8 +19,19 @@ from authentik.policies.models import ( ) from authentik.policies.process import PolicyProcess, cache_key from authentik.policies.types import PolicyRequest, PolicyResult +from authentik.root.monitoring import UpdatingGauge CURRENT_PROCESS = current_process() +GAUGE_POLICIES_CACHED = UpdatingGauge( + "authentik_policies_cached", + "Cached Policies", + update_func=lambda: len(cache.keys("policy_*")), +) +HIST_POLICIES_BUILD_TIME = Histogram( + "authentik_policies_build_time", + "Execution times complete policy result to an object", + ["object_name", "object_type", "user"], +) class PolicyProcessInfo: @@ -92,7 +104,13 @@ class PolicyEngine: def build(self) -> "PolicyEngine": """Build wrapper which monitors performance""" - with Hub.current.start_span(op="policy.engine.build") as span: + with Hub.current.start_span( + op="policy.engine.build" + ) as span, HIST_POLICIES_BUILD_TIME.labels( + object_name=self.__pbm, + object_type=f"{self.__pbm._meta.app_label}.{self.__pbm._meta.model_name}", + user=self.request.user, + ).time(): span: Span span.set_data("pbm", self.__pbm) span.set_data("request", self.request) diff --git a/authentik/policies/models.py b/authentik/policies/models.py index 084a0ca9f..380e6eaa6 100644 --- a/authentik/policies/models.py +++ b/authentik/policies/models.py @@ -111,14 +111,30 @@ class PolicyBinding(SerializerModel): return PolicyBindingSerializer - def __str__(self) -> str: - suffix = "" + @property + def target_type(self) -> str: + """Get the target type this binding is applied to""" if self.policy: - suffix = f"Policy {self.policy.name}" + return "policy" if self.group: - suffix = f"Group {self.group.name}" + return "group" if self.user: - suffix = f"User {self.user.name}" + return "user" + return "invalid" + + @property + def target_name(self) -> str: + """Get the target name this binding is applied to""" + if self.policy: + return self.policy.name + if self.group: + return self.group.name + if self.user: + return self.user.name + return "invalid" + + def __str__(self) -> str: + suffix = f"{self.target_type.title()} {self.target_name}" try: return f"Binding from {self.target} #{self.order} to {suffix}" except PolicyBinding.target.RelatedObjectDoesNotExist: # pylint: disable=no-member diff --git a/authentik/policies/process.py b/authentik/policies/process.py index 263881ca5..cdf859c7f 100644 --- a/authentik/policies/process.py +++ b/authentik/policies/process.py @@ -5,6 +5,7 @@ from traceback import format_tb from typing import Optional from django.core.cache import cache +from prometheus_client import Histogram from sentry_sdk.hub import Hub from sentry_sdk.tracing import Span from structlog.stdlib import get_logger @@ -19,6 +20,18 @@ TRACEBACK_HEADER = "Traceback (most recent call last):\n" FORK_CTX = get_context("fork") PROCESS_CLASS = FORK_CTX.Process +HIST_POLICIES_EXECUTION_TIME = Histogram( + "authentik_policies_execution_time", + "Execution times for single policies", + [ + "binding_order", + "binding_target_type", + "binding_target_name", + "object_name", + "object_type", + "user", + ], +) def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str: @@ -121,7 +134,14 @@ class PolicyProcess(PROCESS_CLASS): """Task wrapper to run policy checking""" with Hub.current.start_span( op="policy.process.execute", - ) as span: + ) as span, HIST_POLICIES_EXECUTION_TIME.labels( + binding_order=self.binding.order, + binding_target_type=self.binding.target_type, + binding_target_name=self.binding.target_name, + object_name=self.request.obj, + object_type=f"{self.request.obj._meta.app_label}.{self.request.obj._meta.model_name}", + user=str(self.request.user), + ).time(): span: Span span.set_data("policy", self.binding.policy) span.set_data("request", self.request) diff --git a/authentik/root/monitoring.py b/authentik/root/monitoring.py index 755ff9842..c7b97ac94 100644 --- a/authentik/root/monitoring.py +++ b/authentik/root/monitoring.py @@ -1,5 +1,6 @@ """Metrics view""" from base64 import b64encode +from typing import Callable from django.conf import settings from django.db import connections @@ -8,8 +9,30 @@ from django.http import HttpRequest, HttpResponse from django.views import View from django_prometheus.exports import ExportToDjangoView from django_redis import get_redis_connection +from prometheus_client import Gauge from redis.exceptions import RedisError +from authentik.admin.api.workers import GAUGE_WORKERS +from authentik.events.monitored_tasks import TaskInfo +from authentik.root.celery import CELERY_APP + + +class UpdatingGauge(Gauge): + """Gauge which fetches its own value from an update function. + + Update function is called on instantiate""" + + def __init__(self, *args, update_func: Callable, **kwargs): + super().__init__(*args, **kwargs) + self._update_func = update_func + self.update() + + def update(self): + """Set value from update function""" + val = self._update_func() + if val: + self.set(val) + class MetricsView(View): """Wrapper around ExportToDjangoView, using http-basic auth""" @@ -20,12 +43,18 @@ class MetricsView(View): auth_type, _, given_credentials = auth_header.partition(" ") credentials = f"monitor:{settings.SECRET_KEY}" expected = b64encode(str.encode(credentials)).decode() - - if auth_type != "Basic" or given_credentials != expected: + authed = auth_type == "Basic" and given_credentials == expected + if not authed and not settings.DEBUG: response = HttpResponse(status=401) response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"' return response + count = len(CELERY_APP.control.ping(timeout=0.5)) + GAUGE_WORKERS.set(count) + + for task in TaskInfo.all().values(): + task.set_prom_metrics() + return ExportToDjangoView(request) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 57fe98c23..86ebb8656 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -257,7 +257,7 @@ CHANNEL_LAYERS = { DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql", + "ENGINE": "django_prometheus.db.backends.postgresql", "HOST": CONFIG.y("postgresql.host"), "NAME": CONFIG.y("postgresql.name"), "USER": CONFIG.y("postgresql.user"), @@ -335,6 +335,10 @@ CELERY_RESULT_BACKEND = ( DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql" +DBBACKUP_CONNECTOR_MAPPING = { + "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", +} + if CONFIG.y("postgresql.s3_backup"): DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" DBBACKUP_STORAGE_OPTIONS = { diff --git a/outpost/go.mod b/outpost/go.mod index a6bbf30a4..4d9254fb5 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/coreos/go-oidc v2.2.1+incompatible - github.com/getsentry/sentry-go v0.10.0 + github.com/getsentry/sentry-go v0.11.0 github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-openapi/analysis v0.20.1 // indirect github.com/go-openapi/errors v0.20.0 // indirect @@ -38,7 +38,7 @@ require ( go.mongodb.org/mongo-driver v1.5.2 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect - golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 + golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.62.0 // indirect diff --git a/outpost/go.sum b/outpost/go.sum index 8ce40262d..1984e11b7 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -130,8 +130,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= -github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= +github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8= +github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= diff --git a/web/package-lock.json b/web/package-lock.json index 37f58f509..9653460a8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -39,7 +39,7 @@ "chartjs-adapter-moment": "^1.0.0", "codemirror": "^5.61.1", "construct-style-sheets-polyfill": "^2.4.16", - "eslint": "^7.26.0", + "eslint": "^7.27.0", "eslint-config-google": "^0.14.0", "eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-lit": "^1.4.1", @@ -48,7 +48,7 @@ "lit-html": "^1.4.1", "moment": "^2.29.1", "rapidoc": "^9.0.0", - "rollup": "^2.48.0", + "rollup": "^2.49.0", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-cssimport": "^1.0.2", @@ -3540,9 +3540,9 @@ } }, "node_modules/eslint": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz", - "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.27.0.tgz", + "integrity": "sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA==", "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.1", @@ -3552,12 +3552,14 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", @@ -3569,7 +3571,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -3578,7 +3580,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^6.0.4", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -3715,6 +3717,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5257,6 +5270,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -6411,9 +6429,9 @@ } }, "node_modules/rollup": { - "version": "2.48.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.48.0.tgz", - "integrity": "sha512-wl9ZSSSsi5579oscSDYSzGn092tCS076YB+TQrzsGuSfYyJeep8eEWj0eaRjuC5McuMNmcnR8icBqiE/FWNB1A==", + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.49.0.tgz", + "integrity": "sha512-UnrCjMXICx9q0jF8L7OYs7LPk95dW0U5UYp/VANnWqfuhyr66FWi/YVlI34Oy8Tp4ZGLcaUDt4APJm80b9oPWQ==", "bin": { "rollup": "dist/bin/rollup" }, @@ -10633,9 +10651,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.26.0.tgz", - "integrity": "sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.27.0.tgz", + "integrity": "sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA==", "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.1", @@ -10645,12 +10663,14 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", "esquery": "^1.4.0", "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", @@ -10662,7 +10682,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -10671,7 +10691,7 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^6.0.4", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -10714,6 +10734,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -12003,6 +12028,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -12914,9 +12944,9 @@ } }, "rollup": { - "version": "2.48.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.48.0.tgz", - "integrity": "sha512-wl9ZSSSsi5579oscSDYSzGn092tCS076YB+TQrzsGuSfYyJeep8eEWj0eaRjuC5McuMNmcnR8icBqiE/FWNB1A==", + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.49.0.tgz", + "integrity": "sha512-UnrCjMXICx9q0jF8L7OYs7LPk95dW0U5UYp/VANnWqfuhyr66FWi/YVlI34Oy8Tp4ZGLcaUDt4APJm80b9oPWQ==", "requires": { "fsevents": "~2.3.1" } diff --git a/web/package.json b/web/package.json index a2503c608..1bd00fd1f 100644 --- a/web/package.json +++ b/web/package.json @@ -68,7 +68,7 @@ "chartjs-adapter-moment": "^1.0.0", "codemirror": "^5.61.1", "construct-style-sheets-polyfill": "^2.4.16", - "eslint": "^7.26.0", + "eslint": "^7.27.0", "eslint-config-google": "^0.14.0", "eslint-plugin-custom-elements": "0.0.2", "eslint-plugin-lit": "^1.4.1", @@ -77,7 +77,7 @@ "lit-html": "^1.4.1", "moment": "^2.29.1", "rapidoc": "^9.0.0", - "rollup": "^2.48.0", + "rollup": "^2.49.0", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-cssimport": "^1.0.2", diff --git a/website/docs/outposts/proxy.mdx b/website/docs/outposts/proxy.mdx index 08e04504a..e12ca02a4 100644 --- a/website/docs/outposts/proxy.mdx +++ b/website/docs/outposts/proxy.mdx @@ -90,6 +90,7 @@ metadata: annotations: nginx.ingress.kubernetes.io/auth-url: http://*external host that you configured in authentik*:4180/akprox/auth?nginx nginx.ingress.kubernetes.io/auth-signin: http://*external host that you configured in authentik*:4180/akprox/start?rd=$escaped_request_uri + nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-Username,X-Forwarded-Email,X-Forwarded-Preferred-Username,X-Forwarded-User nginx.ingress.kubernetes.io/auth-snippet: | proxy_set_header X-Forwarded-Host $http_host; ```