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:
Ildar Iskhakov 2023-08-16 14:13:56 +08:00 committed by GitHub
parent 0335ba2e08
commit ff2db43c49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 131 additions and 10 deletions

View file

@ -336,6 +336,7 @@ services:
required: false
profiles:
- grafana
volumes:
redisdata_dev:
labels: *oncall-labels

View file

@ -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

View file

@ -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",
]

View file

@ -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 = [

View file

@ -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 = []

View 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

View file

@ -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"),
]

View file

@ -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

View file

@ -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",