diff --git a/api/urls.py b/api/urls.py index c5a2e06..272c729 100644 --- a/api/urls.py +++ b/api/urls.py @@ -18,4 +18,5 @@ router.register(r'balance', views.BalanceViewSet, basename='balance') urlpatterns = [ path('', include(router.urls)), path('api-auth/', include('rest_framework.urls')), + path('metrics', views.metrics, name='metrics') ] diff --git a/api/views.py b/api/views.py index c67286f..f0bbdab 100644 --- a/api/views.py +++ b/api/views.py @@ -1,7 +1,10 @@ from typing import Dict, Iterable import dateutil.parser +from django.http import HttpResponse +import prometheus_client import pytz +import core.metric as metric from django.db import transaction from django.utils import timezone from djmoney.money import Money @@ -10,65 +13,184 @@ from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response -from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer, \ - NotificationSerializer, BalanceSerializer, BalanceTransactionSerializer +from api.serializers import ( + InvoiceSerializer, + SimpleInvoiceSerializer, + BillingProjectSerializer, + NotificationSerializer, + BalanceSerializer, + BalanceTransactionSerializer, +) from core.component import component, labels from core.component.component import INVOICE_COMPONENT_MODEL from core.exception import PriceNotFound from core.models import Invoice, BillingProject, Notification, Balance, InvoiceInstance from core.notification import send_notification_from_template -from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED, \ - INVOICE_TAX, COMPANY_NAME, COMPANY_ADDRESS +from core.utils.dynamic_setting import ( + get_dynamic_settings, + get_dynamic_setting, + set_dynamic_setting, + BILLING_ENABLED, + INVOICE_TAX, + COMPANY_NAME, + COMPANY_ADDRESS, +) from core.utils.model_utils import InvoiceComponentMixin from yuyu import settings def get_generic_model_view_set(model): name = type(model).__name__ - meta_params = { - "model": model, - "fields": "__all__" - } + meta_params = {"model": model, "fields": "__all__"} meta_class = type("Meta", (object,), meta_params) - serializer_class = type(f"{name}Serializer", (serializers.ModelSerializer,), {"Meta": meta_class}) + serializer_class = type( + f"{name}Serializer", (serializers.ModelSerializer,), {"Meta": meta_class} + ) view_set_params = { "model": model, "queryset": model.objects, - "serializer_class": serializer_class + "serializer_class": serializer_class, } return type(f"{name}ViewSet", (viewsets.ModelViewSet,), view_set_params) +def metrics(request): + # Cost Explorer + for k, v in INVOICE_COMPONENT_MODEL.items(): + items = v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS).all() + + total_active_resource = v.objects.filter( + invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None + ).count() + + # Total cost this month (All Active Invoice) + sum_of_price = sum([q.price_charged for q in items]) + + sum_of_price = sum_of_price or Money( + amount=0, currency=settings.DEFAULT_CURRENCY + ) + metric.TOTAL_COST_METRIC.labels(job="total_cost", resources=k).set( + sum_of_price.amount + ) + metric.TOTAL_RESOURCE_METRIC.labels(job="total_resource", resources=k).set( + total_active_resource + ) + + # Details Cost + if k == labels.LABEL_INSTANCES: + for i in items: + metric.INSTANCE_COST_METRIC.labels( + job="instance_cost", + flavor=i.flavor_id, + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.INSTANCE_USAGE_METRIC.labels( + job="instance_usage", + flavor=i.flavor_id, + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + if k == labels.LABEL_VOLUMES: + for i in items: + metric.VOLUME_COST_METRIC.labels( + job="volume_cost", + type=i.volume_type_id, + name=i.volume_name, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.VOLUME_USAGE_METRIC.labels( + job="volume_usage", + type=i.volume_type_id, + name=i.volume_name, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + if k == labels.LABEL_FLOATING_IPS: + for i in items: + metric.FLOATINGIP_COST_METRIC.labels( + job="floatingip_cost", + name=i.ip, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.FLOATINGIP_USAGE_METRIC.labels( + job="floatingip_usage", + name=i.ip, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + if k == labels.LABEL_ROUTERS: + for i in items: + metric.ROUTER_COST_METRIC.labels( + job="router_cost", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.ROUTER_USAGE_METRIC.labels( + job="router_usage", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + if k == labels.LABEL_SNAPSHOTS: + for i in items: + metric.SNAPSHOT_COST_METRIC.labels( + job="snapshot_cost", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.SNAPSHOT_USAGE_METRIC.labels( + job="snapshot_usage", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + if k == labels.LABEL_IMAGES: + for i in items: + metric.IMAGE_COST_METRIC.labels( + job="image_cost", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.price_charged.amount) + metric.IMAGE_USAGE_METRIC.labels( + job="image_usage", + name=i.name, + project_id=i.invoice.project.tenant_id, + ).set(i.usage_time) + + metrics_page = prometheus_client.generate_latest() + return HttpResponse( + metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST + ) + + class DynamicSettingViewSet(viewsets.ViewSet): def list(self, request): return Response(get_dynamic_settings()) def retrieve(self, request, pk=None): - return Response({ - pk: get_dynamic_setting(pk) - }) + return Response({pk: get_dynamic_setting(pk)}) def update(self, request, pk=None): - set_dynamic_setting(pk, request.data['value']) - return Response({ - pk: get_dynamic_setting(pk) - }) + set_dynamic_setting(pk, request.data["value"]) + return Response({pk: get_dynamic_setting(pk)}) def partial_update(self, request, pk=None): - set_dynamic_setting(pk, request.data['value']) - return Response({ - pk: get_dynamic_setting(pk) - }) + set_dynamic_setting(pk, request.data["value"]) + return Response({pk: get_dynamic_setting(pk)}) class InvoiceViewSet(viewsets.ModelViewSet): serializer_class = InvoiceSerializer def get_queryset(self): - tenant_id = self.request.query_params.get('tenant_id', None) - return Invoice.objects.filter(project__tenant_id=tenant_id).order_by('-start_date') + tenant_id = self.request.query_params.get("tenant_id", None) + return Invoice.objects.filter(project__tenant_id=tenant_id).order_by( + "-start_date" + ) def parse_time(self, time): dt = dateutil.parser.isoparse(time) @@ -82,34 +204,42 @@ class InvoiceViewSet(viewsets.ModelViewSet): serializer = SimpleInvoiceSerializer(self.get_queryset(), many=True) return Response(serializer.data) - @action(detail=False, methods=['POST']) + @action(detail=False, methods=["POST"]) def enable_billing(self, request): try: self.handle_init_billing(request.data) - return Response({ - "status": "success" - }) + return Response({"status": "success"}) except PriceNotFound as e: - return Response({ - "message": str(e.identifier) + " price not found. Please check price configuration" - }, status=400) + return Response( + { + "message": str(e.identifier) + + " price not found. Please check price configuration" + }, + status=400, + ) - @action(detail=False, methods=['POST']) + @action(detail=False, methods=["POST"]) def disable_billing(self, request): set_dynamic_setting(BILLING_ENABLED, False) - active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all() + active_invoices = Invoice.objects.filter( + state=Invoice.InvoiceState.IN_PROGRESS + ).all() for active_invoice in active_invoices: - self._close_active_invoice(active_invoice, timezone.now(), get_dynamic_setting(INVOICE_TAX)) + self._close_active_invoice( + active_invoice, timezone.now(), get_dynamic_setting(INVOICE_TAX) + ) - return Response({ - "status": "success" - }) + return Response({"status": "success"}) - def _close_active_invoice(self, active_invoice: Invoice, close_date, tax_percentage): + def _close_active_invoice( + self, active_invoice: Invoice, close_date, tax_percentage + ): active_components_map: Dict[str, Iterable[InvoiceComponentMixin]] = {} for label in labels.INVOICE_COMPONENT_LABELS: - active_components_map[label] = getattr(active_invoice, label).filter(end_date=None).all() + active_components_map[label] = ( + getattr(active_invoice, label).filter(end_date=None).all() + ) # Close Invoice Component for active_component in active_components_map[label]: @@ -118,12 +248,10 @@ class InvoiceViewSet(viewsets.ModelViewSet): # Finish current invoice active_invoice.close(close_date, tax_percentage) - @action(detail=False, methods=['POST']) + @action(detail=False, methods=["POST"]) def reset_billing(self, request): self.handle_reset_billing() - return Response({ - "status": "success" - }) + return Response({"status": "success"}) @transaction.atomic def handle_init_billing(self, data): @@ -133,34 +261,37 @@ class InvoiceViewSet(viewsets.ModelViewSet): invoices = {} date_today = timezone.now() - month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + month_first_day = date_today.replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) for name, handler in component.INVOICE_HANDLER.items(): payloads = data[name] for payload in payloads: - - if payload['tenant_id'] not in projects: - project, created = BillingProject.objects.get_or_create(tenant_id=payload['tenant_id']) - projects[payload['tenant_id']] = project - - if payload['tenant_id'] not in invoices: - invoice = Invoice.objects.create( - project=projects[payload['tenant_id']], - start_date=month_first_day, - state=Invoice.InvoiceState.IN_PROGRESS + if payload["tenant_id"] not in projects: + project, created = BillingProject.objects.get_or_create( + tenant_id=payload["tenant_id"] ) - invoices[payload['tenant_id']] = invoice + projects[payload["tenant_id"]] = project - start_date = self.parse_time(payload['start_date']) + if payload["tenant_id"] not in invoices: + invoice = Invoice.objects.create( + project=projects[payload["tenant_id"]], + start_date=month_first_day, + state=Invoice.InvoiceState.IN_PROGRESS, + ) + invoices[payload["tenant_id"]] = invoice + + start_date = self.parse_time(payload["start_date"]) if start_date < month_first_day: start_date = month_first_day - payload['start_date'] = start_date - payload['invoice'] = invoices[payload['tenant_id']] + payload["start_date"] = start_date + payload["invoice"] = invoices[payload["tenant_id"]] # create not accepting tenant_id, delete it - del payload['tenant_id'] + del payload["tenant_id"] handler.create(payload, fallback_price=True) @transaction.atomic @@ -178,7 +309,7 @@ class InvoiceViewSet(viewsets.ModelViewSet): def finish(self, request, pk): invoice = Invoice.objects.filter(id=pk).first() balance = Balance.get_balance_for_project(invoice.project) - skip_balance = self.request.query_params.get('skip_balance', '') + skip_balance = self.request.query_params.get("skip_balance", "") if invoice.state == Invoice.InvoiceState.UNPAID: if not skip_balance: @@ -188,14 +319,14 @@ class InvoiceViewSet(viewsets.ModelViewSet): send_notification_from_template( project=invoice.project, - title=settings.EMAIL_TAG + f' Invoice #{invoice.id} has been Paid', - short_description=f'Invoice is paid with total of {invoice.total}', - template='invoice.html', + title=settings.EMAIL_TAG + f" Invoice #{invoice.id} has been Paid", + short_description=f"Invoice is paid with total of {invoice.total}", + template="invoice.html", context={ - 'invoice': invoice, - 'company_name': get_dynamic_setting(COMPANY_NAME), - 'address': get_dynamic_setting(COMPANY_ADDRESS), - } + "invoice": invoice, + "company_name": get_dynamic_setting(COMPANY_NAME), + "address": get_dynamic_setting(COMPANY_ADDRESS), + }, ) serializer = InvoiceSerializer(invoice) @@ -205,7 +336,7 @@ class InvoiceViewSet(viewsets.ModelViewSet): def rollback_to_unpaid(self, request, pk): invoice = Invoice.objects.filter(id=pk).first() balance = Balance.get_balance_for_project(invoice.project) - skip_balance = self.request.query_params.get('skip_balance', '') + skip_balance = self.request.query_params.get("skip_balance", "") if invoice.state == Invoice.InvoiceState.FINISHED: if not skip_balance: # Refund @@ -215,14 +346,14 @@ class InvoiceViewSet(viewsets.ModelViewSet): send_notification_from_template( project=invoice.project, - title=settings.EMAIL_TAG + f' Invoice #{invoice.id} is Unpaid', - short_description=f'Invoice is Unpaid with total of {invoice.total}', - template='invoice.html', + title=settings.EMAIL_TAG + f" Invoice #{invoice.id} is Unpaid", + short_description=f"Invoice is Unpaid with total of {invoice.total}", + template="invoice.html", context={ - 'invoice': invoice, - 'company_name': get_dynamic_setting(COMPANY_NAME), - 'address': get_dynamic_setting(COMPANY_ADDRESS), - } + "invoice": invoice, + "company_name": get_dynamic_setting(COMPANY_NAME), + "address": get_dynamic_setting(COMPANY_ADDRESS), + }, ) serializer = InvoiceSerializer(invoice) @@ -233,60 +364,83 @@ class AdminOverviewViewSet(viewsets.ViewSet): def list(self, request): return Response({}) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def total_resource(self, request): data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - data['label'].append(k) - data['data'].append(v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS).count()) + data["label"].append(k) + data["data"].append( + v.objects.filter( + invoice__state=Invoice.InvoiceState.IN_PROGRESS + ).count() + ) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def active_resource(self, request): data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - data['label'].append(k) - data['data'].append( - v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).count()) + data["label"].append(k) + data["data"].append( + v.objects.filter( + invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None + ).count() + ) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def price_total_resource(self, request): data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - sum_of_price = sum([q.price_charged for q in - v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS).all()]) + sum_of_price = sum( + [ + q.price_charged + for q in v.objects.filter( + invoice__state=Invoice.InvoiceState.IN_PROGRESS + ).all() + ] + ) - sum_of_price = sum_of_price or Money(amount=0, currency=settings.DEFAULT_CURRENCY) - data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') - data['data'].append(sum_of_price.amount) + sum_of_price = sum_of_price or Money( + amount=0, currency=settings.DEFAULT_CURRENCY + ) + data["label"].append(k + " (" + str(sum_of_price.currency) + ")") + data["data"].append(sum_of_price.amount) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def price_active_resource(self, request): data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - sum_of_price = sum([q.price_charged for q in - v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).all()]) + sum_of_price = sum( + [ + q.price_charged + for q in v.objects.filter( + invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None + ).all() + ] + ) - sum_of_price = sum_of_price or Money(amount=0, currency=settings.DEFAULT_CURRENCY) - data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') - data['data'].append(sum_of_price.amount) + sum_of_price = sum_of_price or Money( + amount=0, currency=settings.DEFAULT_CURRENCY + ) + data["label"].append(k + " (" + str(sum_of_price.currency) + ")") + data["data"].append(sum_of_price.amount) return Response(data) @@ -298,90 +452,113 @@ class ProjectOverviewViewSet(viewsets.ViewSet): return Response(serializer.data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def get_tenant(self, request, pk): project = BillingProject.objects.filter(tenant_id=pk).first() serializer = BillingProjectSerializer(project) return Response(serializer.data) - @action(detail=True, methods=['POST']) + @action(detail=True, methods=["POST"]) def update_email(self, request, pk): project = BillingProject.objects.filter(tenant_id=pk).first() serializer = BillingProjectSerializer(project, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response({ - "status": "success" - }) + return Response({"status": "success"}) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def total_resource(self, request): - tenant_id = self.request.query_params.get('tenant_id', None) + tenant_id = self.request.query_params.get("tenant_id", None) project = BillingProject.objects.filter(tenant_id=tenant_id).first() data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - data['label'].append(k) - data['data'].append( - v.objects.filter(invoice__project=project, invoice__state=Invoice.InvoiceState.IN_PROGRESS).count()) + data["label"].append(k) + data["data"].append( + v.objects.filter( + invoice__project=project, + invoice__state=Invoice.InvoiceState.IN_PROGRESS, + ).count() + ) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def active_resource(self, request): - tenant_id = self.request.query_params.get('tenant_id', None) + tenant_id = self.request.query_params.get("tenant_id", None) project = BillingProject.objects.filter(tenant_id=tenant_id).first() data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - data['label'].append(k) - data['data'].append( - v.objects.filter(invoice__project=project, invoice__state=Invoice.InvoiceState.IN_PROGRESS, - end_date=None).count()) + data["label"].append(k) + data["data"].append( + v.objects.filter( + invoice__project=project, + invoice__state=Invoice.InvoiceState.IN_PROGRESS, + end_date=None, + ).count() + ) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def price_total_resource(self, request): - tenant_id = self.request.query_params.get('tenant_id', None) + tenant_id = self.request.query_params.get("tenant_id", None) project = BillingProject.objects.filter(tenant_id=tenant_id).first() data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - sum_of_price = sum([q.price_charged for q in - v.objects.filter(invoice__project=project, - invoice__state=Invoice.InvoiceState.IN_PROGRESS).all()]) + sum_of_price = sum( + [ + q.price_charged + for q in v.objects.filter( + invoice__project=project, + invoice__state=Invoice.InvoiceState.IN_PROGRESS, + ).all() + ] + ) - sum_of_price = sum_of_price or Money(amount=0, currency=settings.DEFAULT_CURRENCY) - data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') - data['data'].append(sum_of_price.amount) + sum_of_price = sum_of_price or Money( + amount=0, currency=settings.DEFAULT_CURRENCY + ) + data["label"].append(k + " (" + str(sum_of_price.currency) + ")") + data["data"].append(sum_of_price.amount) return Response(data) - @action(detail=False, methods=['GET']) + @action(detail=False, methods=["GET"]) def price_active_resource(self, request): - tenant_id = self.request.query_params.get('tenant_id', None) + tenant_id = self.request.query_params.get("tenant_id", None) project = BillingProject.objects.filter(tenant_id=tenant_id).first() data = { - 'label': [], - 'data': [], + "label": [], + "data": [], } for k, v in INVOICE_COMPONENT_MODEL.items(): - sum_of_price = sum([q.price_charged for q in - v.objects.filter(invoice__project=project, - invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).all()]) + sum_of_price = sum( + [ + q.price_charged + for q in v.objects.filter( + invoice__project=project, + invoice__state=Invoice.InvoiceState.IN_PROGRESS, + end_date=None, + ).all() + ] + ) - sum_of_price = sum_of_price or Money(amount=0, currency=settings.DEFAULT_CURRENCY) - data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') - data['data'].append(sum_of_price.amount) + sum_of_price = sum_of_price or Money( + amount=0, currency=settings.DEFAULT_CURRENCY + ) + data["label"].append(k + " (" + str(sum_of_price.currency) + ")") + data["data"].append(sum_of_price.amount) return Response(data) @@ -390,13 +567,15 @@ class NotificationViewSet(viewsets.ModelViewSet): serializer_class = NotificationSerializer def get_queryset(self): - tenant_id = self.request.query_params.get('tenant_id', None) + tenant_id = self.request.query_params.get("tenant_id", None) if tenant_id is None: - return Notification.objects.order_by('-created_at') - if tenant_id == '0': - return Notification.objects.filter(project=None).order_by('-created_at') + return Notification.objects.order_by("-created_at") + if tenant_id == "0": + return Notification.objects.filter(project=None).order_by("-created_at") - return Notification.objects.filter(project__tenant_id=tenant_id).order_by('-created_at') + return Notification.objects.filter(project__tenant_id=tenant_id).order_by( + "-created_at" + ) def retrieve(self, request, *args, **kwargs): instance = self.get_object() @@ -405,7 +584,7 @@ class NotificationViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(instance) return Response(serializer.data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def resend(self, request, pk): notification = Notification.objects.filter(id=pk).first() notification.send() @@ -414,7 +593,7 @@ class NotificationViewSet(viewsets.ModelViewSet): return Response(serializer.data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def set_read(self, request, pk): notification = Notification.objects.filter(id=pk).first() notification.is_read = True @@ -424,7 +603,7 @@ class NotificationViewSet(viewsets.ModelViewSet): return Response(serializer.data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def set_unread(self, request, pk): notification = Notification.objects.filter(id=pk).first() notification.is_read = False @@ -447,34 +626,43 @@ class BalanceViewSet(viewsets.ViewSet): balance = Balance.objects.filter(pk).first() return Response(BalanceSerializer(balance).data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def retrieve_by_project(self, request, pk): project = get_object_or_404(BillingProject, tenant_id=pk) balance = Balance.get_balance_for_project(project=project) return Response(BalanceSerializer(balance).data) - @action(detail=True, methods=['GET']) + @action(detail=True, methods=["GET"]) def transaction_by_project(self, request, pk): project = get_object_or_404(BillingProject, tenant_id=pk) - transactions = Balance.get_balance_for_project(project=project).balancetransaction_set + transactions = Balance.get_balance_for_project( + project=project + ).balancetransaction_set return Response(BalanceTransactionSerializer(transactions, many=True).data) - @action(detail=True, methods=['POST']) + @action(detail=True, methods=["POST"]) def top_up_by_project(self, request, pk): project = get_object_or_404(BillingProject, tenant_id=pk) balance = Balance.get_balance_for_project(project=project) balance_transaction = balance.top_up( - Money(amount=request.data['amount'], currency=request.data['amount_currency']), - description=request.data['description']) + Money( + amount=request.data["amount"], currency=request.data["amount_currency"] + ), + description=request.data["description"], + ) return Response(BalanceTransactionSerializer(balance_transaction).data) - @action(detail=True, methods=['POST']) + @action(detail=True, methods=["POST"]) def top_down_by_project(self, request, pk): project = get_object_or_404(BillingProject, tenant_id=pk) balance = Balance.get_balance_for_project(project=project) balance_transaction = balance.top_down( - Money(amount=request.data['amount'], currency=request.data['amount_currency']), - description=request.data['description']) + Money( + amount=request.data["amount"], currency=request.data["amount_currency"] + ), + description=request.data["description"], + ) return Response(BalanceTransactionSerializer(balance_transaction).data) + # endregion diff --git a/core/metric.py b/core/metric.py new file mode 100644 index 0000000..73d938d --- /dev/null +++ b/core/metric.py @@ -0,0 +1,30 @@ +import prometheus_client + +from core.component.component import INVOICE_COMPONENT_MODEL +from core.component.labels import INVOICE_COMPONENT_LABELS + +# Cost Explorer (Summary) +TOTAL_COST_METRIC = prometheus_client.Gauge('total_cost', 'Total Cost of resource for current invoice period', labelnames=['job', 'resources']) + +# Detailed Cost +INSTANCE_COST_METRIC = prometheus_client.Gauge('instance_cost', 'Cost of instance usage', labelnames=['job', 'flavor', 'name', 'project_id']) +INSTANCE_USAGE_METRIC = prometheus_client.Gauge('instance_usage', 'Usage time of instance', labelnames=['job', 'flavor', 'name', 'project_id']) + +VOLUME_COST_METRIC = prometheus_client.Gauge('volume_cost', 'Cost of volume usage', labelnames=['job', 'type', 'name', 'project_id']) +VOLUME_USAGE_METRIC = prometheus_client.Gauge('volume_usage', 'Usage time of volume', labelnames=['job', 'type', 'name', 'project_id']) + +FLOATINGIP_COST_METRIC = prometheus_client.Gauge('floatingip_cost', 'Cost of floatingip usage', labelnames=['job', 'name', 'project_id']) +FLOATINGIP_USAGE_METRIC = prometheus_client.Gauge('floatingip_usage', 'Usage time of floatingip', labelnames=['job', 'name', 'project_id']) + +ROUTER_COST_METRIC = prometheus_client.Gauge('router_cost', 'Cost of router usage', labelnames=['job', 'name', 'project_id']) +ROUTER_USAGE_METRIC = prometheus_client.Gauge('router_usage', 'Usage time of router', labelnames=['job', 'name', 'project_id']) + +SNAPSHOT_COST_METRIC = prometheus_client.Gauge('snapshot_cost', 'Cost of snapshot usage', labelnames=['job', 'name', 'project_id']) +SNAPSHOT_USAGE_METRIC = prometheus_client.Gauge('snapshot_usage', 'Usage time of snapshot', labelnames=['job', 'name', 'project_id']) + +IMAGE_COST_METRIC = prometheus_client.Gauge('image_cost', 'Cost of image usage', labelnames=['job', 'name', 'project_id']) +IMAGE_USAGE_METRIC = prometheus_client.Gauge('image_usage', 'Usage time of image', labelnames=['job', 'name', 'project_id']) + +# Resource growth +TOTAL_RESOURCE_METRIC = prometheus_client.Gauge('total_resource', 'Total Active Resource', labelnames=['job', 'resources']) + diff --git a/core/utils/model_utils.py b/core/utils/model_utils.py index c581b82..7d3fae7 100644 --- a/core/utils/model_utils.py +++ b/core/utils/model_utils.py @@ -3,6 +3,7 @@ import math from django.db import models from django.utils import timezone from djmoney.models.fields import MoneyField +from django.utils.timesince import timesince class BaseModel(models.Model): @@ -70,6 +71,11 @@ class InvoiceComponentMixin(TimestampMixin, PriceMixin): return self.hourly_price * hour_passes + @property + def usage_time(self): + usage = self.adjusted_end_date - self.start_date + return usage.seconds + def is_closed(self): """ Is component closed diff --git a/requirements.txt b/requirements.txt index b9b2d3d..6345569 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ openstacksdk==0.58.0 djangorestframework==3.12.4 django-filter==2.4.0 Markdown==3.3.4 -gunicorn==20.1.0 \ No newline at end of file +gunicorn==20.1.0 +prometheus-client==0.16.0 \ No newline at end of file diff --git a/yuyu/urls.py b/yuyu/urls.py index a82f32e..d32471c 100644 --- a/yuyu/urls.py +++ b/yuyu/urls.py @@ -15,8 +15,10 @@ Including another URLconf """ from django.contrib import admin from django.urls import path, include +from api import views urlpatterns = [ path('admin/', admin.site.urls), - path('api/', include('api.urls')) + path('api/', include('api.urls')), + path('metrics', views.metrics) ]