Add openapi schema generation for internal api (#2771)
# What this PR does ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
0335ba2e08
commit
ff2db43c49
9 changed files with 131 additions and 10 deletions
|
|
@ -336,6 +336,7 @@ services:
|
|||
required: false
|
||||
profiles:
|
||||
- grafana
|
||||
|
||||
volumes:
|
||||
redisdata_dev:
|
||||
labels: *oncall-labels
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import extend_schema_field, inline_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.classic_markdown_renderer import AlertGroupClassicMarkdownRenderer
|
||||
|
|
@ -13,7 +15,7 @@ from common.api_helpers.mixins import EagerLoadingMixin
|
|||
from .alert import AlertSerializer
|
||||
from .alert_receive_channel import FastAlertReceiveChannelSerializer
|
||||
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
|
||||
from .user import FastUserSerializer
|
||||
from .user import FastUserSerializer, UserShortSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -62,7 +64,19 @@ class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializer
|
|||
class Meta:
|
||||
model = AlertGroup
|
||||
fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
|
||||
read_only_fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
|
||||
|
||||
@extend_schema_field(
|
||||
inline_serializer(
|
||||
name="render_for_web",
|
||||
fields={
|
||||
"title": serializers.CharField(),
|
||||
"message": serializers.CharField(),
|
||||
"image_url": serializers.CharField(),
|
||||
"source_link": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
)
|
||||
def get_render_for_web(self, obj):
|
||||
last_alert = obj.alerts.last()
|
||||
if last_alert is None:
|
||||
|
|
@ -138,6 +152,17 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize
|
|||
"is_restricted",
|
||||
]
|
||||
|
||||
@extend_schema_field(
|
||||
inline_serializer(
|
||||
name="render_for_web",
|
||||
fields={
|
||||
"title": serializers.CharField(),
|
||||
"message": serializers.CharField(),
|
||||
"image_url": serializers.CharField(),
|
||||
"source_link": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
)
|
||||
def get_render_for_web(self, obj):
|
||||
if not obj.last_alert:
|
||||
return {}
|
||||
|
|
@ -149,6 +174,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize
|
|||
)
|
||||
|
||||
def get_render_for_classic_markdown(self, obj):
|
||||
"""Deprecated. TODO: remove"""
|
||||
if not obj.last_alert:
|
||||
return {}
|
||||
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
|
||||
|
|
@ -158,6 +184,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize
|
|||
AlertGroupClassicMarkdownRenderer,
|
||||
)
|
||||
|
||||
@extend_schema_field(UserShortSerializer(many=True))
|
||||
def get_related_users(self, obj):
|
||||
users_ids = set()
|
||||
users = []
|
||||
|
|
@ -166,21 +193,21 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize
|
|||
# when def acknowledge/resolve are called in view.
|
||||
if obj.resolved_by_user:
|
||||
users_ids.add(obj.resolved_by_user.public_primary_key)
|
||||
users.append(obj.resolved_by_user.short())
|
||||
users.append(obj.resolved_by_user)
|
||||
|
||||
if obj.acknowledged_by_user and obj.acknowledged_by_user.public_primary_key not in users_ids:
|
||||
users_ids.add(obj.acknowledged_by_user.public_primary_key)
|
||||
users.append(obj.acknowledged_by_user.short())
|
||||
users.append(obj.acknowledged_by_user)
|
||||
|
||||
if obj.silenced_by_user and obj.silenced_by_user.public_primary_key not in users_ids:
|
||||
users_ids.add(obj.silenced_by_user.public_primary_key)
|
||||
users.append(obj.silenced_by_user.short())
|
||||
users.append(obj.silenced_by_user)
|
||||
|
||||
for log_record in obj.log_records.all():
|
||||
if log_record.author is not None and log_record.author.public_primary_key not in users_ids:
|
||||
users.append(log_record.author.short())
|
||||
users.append(log_record.author)
|
||||
users_ids.add(log_record.author.public_primary_key)
|
||||
return users
|
||||
return UserShortSerializer(users, many=True).data
|
||||
|
||||
|
||||
class AlertGroupSerializer(AlertGroupListSerializer):
|
||||
|
|
@ -198,7 +225,7 @@ class AlertGroupSerializer(AlertGroupListSerializer):
|
|||
"paged_users",
|
||||
]
|
||||
|
||||
def get_last_alert_at(self, obj):
|
||||
def get_last_alert_at(self, obj) -> datetime.datetime:
|
||||
last_alert = obj.alerts.last()
|
||||
|
||||
if not last_alert:
|
||||
|
|
@ -206,6 +233,7 @@ class AlertGroupSerializer(AlertGroupListSerializer):
|
|||
|
||||
return last_alert.created_at
|
||||
|
||||
@extend_schema_field(AlertSerializer(many=True))
|
||||
def get_limited_alerts(self, obj):
|
||||
"""
|
||||
Overriding default alerts because there are alert_groups with thousands of them.
|
||||
|
|
@ -214,5 +242,8 @@ class AlertGroupSerializer(AlertGroupListSerializer):
|
|||
alerts = obj.alerts.order_by("-pk")[:100]
|
||||
return AlertSerializer(alerts, many=True).data
|
||||
|
||||
@extend_schema_field(UserShortSerializer(many=True))
|
||||
def get_paged_users(self, obj):
|
||||
return [u.short() for u in obj.get_paged_users()]
|
||||
paged_users = obj.get_paged_users()
|
||||
serializer = UserShortSerializer(paged_users, many=True)
|
||||
return serializer.data
|
||||
|
|
|
|||
|
|
@ -239,3 +239,25 @@ class FilterUserSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
"pk",
|
||||
"username",
|
||||
]
|
||||
|
||||
|
||||
class UserShortSerializer(serializers.ModelSerializer):
|
||||
username = serializers.CharField()
|
||||
pk = serializers.CharField(source="public_primary_key")
|
||||
avatar = serializers.CharField(source="avatar_url")
|
||||
avatar_full = serializers.CharField(source="avatar_full_url")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
"username",
|
||||
"pk",
|
||||
"avatar",
|
||||
"avatar_full",
|
||||
]
|
||||
read_only_fields = [
|
||||
"username",
|
||||
"pk",
|
||||
"avatar",
|
||||
"avatar_full",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from django.db.models import Count, Max, Q
|
|||
from django.utils import timezone
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.widgets import RangeWidget
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
|
@ -395,8 +396,10 @@ class AlertGroupView(
|
|||
|
||||
return alert_groups
|
||||
|
||||
@extend_schema(responses=inline_serializer(name="AlertGroupStats", fields={"count": serializers.IntegerField()}))
|
||||
@action(detail=False)
|
||||
def stats(self, *args, **kwargs):
|
||||
"""Return number of alert groups capped at 100001"""
|
||||
MAX_COUNT = 100001
|
||||
alert_groups = self.filter_queryset(self.get_queryset())[:MAX_COUNT]
|
||||
count = alert_groups.count()
|
||||
|
|
@ -492,6 +495,9 @@ class AlertGroupView(
|
|||
|
||||
@action(methods=["post"], detail=True)
|
||||
def attach(self, request, pk=None):
|
||||
"""
|
||||
Attach alert group to another alert group
|
||||
"""
|
||||
alert_group = self.get_object()
|
||||
if alert_group.is_maintenance_incident:
|
||||
raise BadRequest(detail="Can't attach maintenance alert group")
|
||||
|
|
@ -537,6 +543,13 @@ class AlertGroupView(
|
|||
alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="silence_options",
|
||||
fields={"value": serializers.CharField(), "display_name": serializers.CharField()},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def silence_options(self, request):
|
||||
data = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from django.conf import settings
|
||||
from drf_spectacular.utils import OpenApiExample, extend_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
|
@ -21,8 +23,19 @@ class FeaturesAPIView(APIView):
|
|||
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
|
||||
@extend_schema(
|
||||
request=None,
|
||||
responses=serializers.ListField(child=serializers.CharField()),
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Example response",
|
||||
value=["slack", "telegram", "grafana_cloud_connection", "live_settings", "grafana_cloud_notifications"],
|
||||
)
|
||||
],
|
||||
)
|
||||
def get(self, request):
|
||||
return Response(self._get_enabled_features(request))
|
||||
data = self._get_enabled_features(request)
|
||||
return Response(data)
|
||||
|
||||
def _get_enabled_features(self, request):
|
||||
enabled_features = []
|
||||
|
|
|
|||
9
engine/engine/included_path.py
Normal file
9
engine/engine/included_path.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
def custom_preprocessing_hook(endpoints):
|
||||
filtered = []
|
||||
for path, path_regex, method, callback in endpoints:
|
||||
if any(path_prefix in path for path_prefix in settings.SPECTACULAR_INCLUDED_PATHS):
|
||||
filtered.append((path, path_regex, method, callback))
|
||||
return filtered
|
||||
|
|
@ -73,3 +73,11 @@ if settings.DEBUG:
|
|||
|
||||
if settings.SILK_PROFILER_ENABLED:
|
||||
urlpatterns += [path(settings.SILK_PATH, include("silk.urls", namespace="silk"))]
|
||||
|
||||
if settings.DRF_SPECTACULAR_ENABLED:
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
|
||||
urlpatterns += [
|
||||
path("internal/schema/", SpectacularAPIView.as_view(api_version="internal/v1"), name="schema"),
|
||||
path("internal/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -57,3 +57,4 @@ urllib3==1.26.15
|
|||
prometheus_client==0.16.0
|
||||
lxml==4.9.2
|
||||
babel==2.12.1
|
||||
drf-spectacular==0.26.2
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ INSTALLED_APPS = [
|
|||
"fcm_django",
|
||||
"django_dbconn_retry",
|
||||
"apps.phone_notifications",
|
||||
"drf_spectacular",
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
|
|
@ -246,8 +247,30 @@ REST_FRAMEWORK = {
|
|||
"rest_framework.parsers.MultiPartParser",
|
||||
),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [],
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
|
||||
DRF_SPECTACULAR_ENABLED = getenv_boolean("DRF_SPECTACULAR_ENABLED", default=False)
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Grafana OnCall Private API",
|
||||
"DESCRIPTION": "Internal API docs. This is not meant to be used by end users. API endpoints will be kept added/removed/changed without notice.",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
# OTHER SETTINGS
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"engine.included_path.custom_preprocessing_hook"
|
||||
], # Custom hook to include only paths from SPECTACULAR_INCLUDED_PATHS
|
||||
"SERVE_URLCONF": ("apps.api.urls"),
|
||||
"SWAGGER_UI_SETTINGS": {"supportedSubmitMethods": []}, # Disable "Try it out" button for all endpoints
|
||||
}
|
||||
|
||||
SPECTACULAR_INCLUDED_PATHS = [
|
||||
"/features",
|
||||
"/alertgroups",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"log_request_id.middleware.RequestIDMiddleware",
|
||||
"engine.middlewares.RequestTimeLoggingMiddleware",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue