feat: Prometheus Metric exporter (#4)

* feat: Metric Exporter

* add project_id to metric
This commit is contained in:
Setyo Nugroho 2023-10-02 11:28:16 +07:00 committed by GitHub
parent be42c2fd9d
commit 6c98855c98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 388 additions and 160 deletions

View file

@ -18,4 +18,5 @@ router.register(r'balance', views.BalanceViewSet, basename='balance')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls')), path('api-auth/', include('rest_framework.urls')),
path('metrics', views.metrics, name='metrics')
] ]

View file

@ -1,7 +1,10 @@
from typing import Dict, Iterable from typing import Dict, Iterable
import dateutil.parser import dateutil.parser
from django.http import HttpResponse
import prometheus_client
import pytz import pytz
import core.metric as metric
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from djmoney.money import Money 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.generics import get_object_or_404
from rest_framework.response import Response from rest_framework.response import Response
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer, \ from api.serializers import (
NotificationSerializer, BalanceSerializer, BalanceTransactionSerializer InvoiceSerializer,
SimpleInvoiceSerializer,
BillingProjectSerializer,
NotificationSerializer,
BalanceSerializer,
BalanceTransactionSerializer,
)
from core.component import component, labels from core.component import component, labels
from core.component.component import INVOICE_COMPONENT_MODEL from core.component.component import INVOICE_COMPONENT_MODEL
from core.exception import PriceNotFound from core.exception import PriceNotFound
from core.models import Invoice, BillingProject, Notification, Balance, InvoiceInstance from core.models import Invoice, BillingProject, Notification, Balance, InvoiceInstance
from core.notification import send_notification_from_template 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, \ from core.utils.dynamic_setting import (
INVOICE_TAX, COMPANY_NAME, COMPANY_ADDRESS 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 core.utils.model_utils import InvoiceComponentMixin
from yuyu import settings from yuyu import settings
def get_generic_model_view_set(model): def get_generic_model_view_set(model):
name = type(model).__name__ name = type(model).__name__
meta_params = { meta_params = {"model": model, "fields": "__all__"}
"model": model,
"fields": "__all__"
}
meta_class = type("Meta", (object,), meta_params) 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 = { view_set_params = {
"model": model, "model": model,
"queryset": model.objects, "queryset": model.objects,
"serializer_class": serializer_class "serializer_class": serializer_class,
} }
return type(f"{name}ViewSet", (viewsets.ModelViewSet,), view_set_params) 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): class DynamicSettingViewSet(viewsets.ViewSet):
def list(self, request): def list(self, request):
return Response(get_dynamic_settings()) return Response(get_dynamic_settings())
def retrieve(self, request, pk=None): def retrieve(self, request, pk=None):
return Response({ return Response({pk: get_dynamic_setting(pk)})
pk: get_dynamic_setting(pk)
})
def update(self, request, pk=None): def update(self, request, pk=None):
set_dynamic_setting(pk, request.data['value']) set_dynamic_setting(pk, request.data["value"])
return Response({ return Response({pk: get_dynamic_setting(pk)})
pk: get_dynamic_setting(pk)
})
def partial_update(self, request, pk=None): def partial_update(self, request, pk=None):
set_dynamic_setting(pk, request.data['value']) set_dynamic_setting(pk, request.data["value"])
return Response({ return Response({pk: get_dynamic_setting(pk)})
pk: get_dynamic_setting(pk)
})
class InvoiceViewSet(viewsets.ModelViewSet): class InvoiceViewSet(viewsets.ModelViewSet):
serializer_class = InvoiceSerializer serializer_class = InvoiceSerializer
def get_queryset(self): def get_queryset(self):
tenant_id = self.request.query_params.get('tenant_id', None) tenant_id = self.request.query_params.get("tenant_id", None)
return Invoice.objects.filter(project__tenant_id=tenant_id).order_by('-start_date') return Invoice.objects.filter(project__tenant_id=tenant_id).order_by(
"-start_date"
)
def parse_time(self, time): def parse_time(self, time):
dt = dateutil.parser.isoparse(time) dt = dateutil.parser.isoparse(time)
@ -82,34 +204,42 @@ class InvoiceViewSet(viewsets.ModelViewSet):
serializer = SimpleInvoiceSerializer(self.get_queryset(), many=True) serializer = SimpleInvoiceSerializer(self.get_queryset(), many=True)
return Response(serializer.data) return Response(serializer.data)
@action(detail=False, methods=['POST']) @action(detail=False, methods=["POST"])
def enable_billing(self, request): def enable_billing(self, request):
try: try:
self.handle_init_billing(request.data) self.handle_init_billing(request.data)
return Response({ return Response({"status": "success"})
"status": "success"
})
except PriceNotFound as e: except PriceNotFound as e:
return Response({ return Response(
"message": str(e.identifier) + " price not found. Please check price configuration" {
}, status=400) "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): def disable_billing(self, request):
set_dynamic_setting(BILLING_ENABLED, False) 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: 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({ return Response({"status": "success"})
"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]] = {} active_components_map: Dict[str, Iterable[InvoiceComponentMixin]] = {}
for label in labels.INVOICE_COMPONENT_LABELS: 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 # Close Invoice Component
for active_component in active_components_map[label]: for active_component in active_components_map[label]:
@ -118,12 +248,10 @@ class InvoiceViewSet(viewsets.ModelViewSet):
# Finish current invoice # Finish current invoice
active_invoice.close(close_date, tax_percentage) active_invoice.close(close_date, tax_percentage)
@action(detail=False, methods=['POST']) @action(detail=False, methods=["POST"])
def reset_billing(self, request): def reset_billing(self, request):
self.handle_reset_billing() self.handle_reset_billing()
return Response({ return Response({"status": "success"})
"status": "success"
})
@transaction.atomic @transaction.atomic
def handle_init_billing(self, data): def handle_init_billing(self, data):
@ -133,34 +261,37 @@ class InvoiceViewSet(viewsets.ModelViewSet):
invoices = {} invoices = {}
date_today = timezone.now() 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(): for name, handler in component.INVOICE_HANDLER.items():
payloads = data[name] payloads = data[name]
for payload in payloads: for payload in payloads:
if payload["tenant_id"] not in projects:
if payload['tenant_id'] not in projects: project, created = BillingProject.objects.get_or_create(
project, created = BillingProject.objects.get_or_create(tenant_id=payload['tenant_id']) 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
) )
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: if start_date < month_first_day:
start_date = month_first_day start_date = month_first_day
payload['start_date'] = start_date payload["start_date"] = start_date
payload['invoice'] = invoices[payload['tenant_id']] payload["invoice"] = invoices[payload["tenant_id"]]
# create not accepting tenant_id, delete it # create not accepting tenant_id, delete it
del payload['tenant_id'] del payload["tenant_id"]
handler.create(payload, fallback_price=True) handler.create(payload, fallback_price=True)
@transaction.atomic @transaction.atomic
@ -178,7 +309,7 @@ class InvoiceViewSet(viewsets.ModelViewSet):
def finish(self, request, pk): def finish(self, request, pk):
invoice = Invoice.objects.filter(id=pk).first() invoice = Invoice.objects.filter(id=pk).first()
balance = Balance.get_balance_for_project(invoice.project) 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 invoice.state == Invoice.InvoiceState.UNPAID:
if not skip_balance: if not skip_balance:
@ -188,14 +319,14 @@ class InvoiceViewSet(viewsets.ModelViewSet):
send_notification_from_template( send_notification_from_template(
project=invoice.project, project=invoice.project,
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} has been Paid', title=settings.EMAIL_TAG + f" Invoice #{invoice.id} has been Paid",
short_description=f'Invoice is paid with total of {invoice.total}', short_description=f"Invoice is paid with total of {invoice.total}",
template='invoice.html', template="invoice.html",
context={ context={
'invoice': invoice, "invoice": invoice,
'company_name': get_dynamic_setting(COMPANY_NAME), "company_name": get_dynamic_setting(COMPANY_NAME),
'address': get_dynamic_setting(COMPANY_ADDRESS), "address": get_dynamic_setting(COMPANY_ADDRESS),
} },
) )
serializer = InvoiceSerializer(invoice) serializer = InvoiceSerializer(invoice)
@ -205,7 +336,7 @@ class InvoiceViewSet(viewsets.ModelViewSet):
def rollback_to_unpaid(self, request, pk): def rollback_to_unpaid(self, request, pk):
invoice = Invoice.objects.filter(id=pk).first() invoice = Invoice.objects.filter(id=pk).first()
balance = Balance.get_balance_for_project(invoice.project) 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 invoice.state == Invoice.InvoiceState.FINISHED:
if not skip_balance: if not skip_balance:
# Refund # Refund
@ -215,14 +346,14 @@ class InvoiceViewSet(viewsets.ModelViewSet):
send_notification_from_template( send_notification_from_template(
project=invoice.project, project=invoice.project,
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} is Unpaid', title=settings.EMAIL_TAG + f" Invoice #{invoice.id} is Unpaid",
short_description=f'Invoice is Unpaid with total of {invoice.total}', short_description=f"Invoice is Unpaid with total of {invoice.total}",
template='invoice.html', template="invoice.html",
context={ context={
'invoice': invoice, "invoice": invoice,
'company_name': get_dynamic_setting(COMPANY_NAME), "company_name": get_dynamic_setting(COMPANY_NAME),
'address': get_dynamic_setting(COMPANY_ADDRESS), "address": get_dynamic_setting(COMPANY_ADDRESS),
} },
) )
serializer = InvoiceSerializer(invoice) serializer = InvoiceSerializer(invoice)
@ -233,60 +364,83 @@ class AdminOverviewViewSet(viewsets.ViewSet):
def list(self, request): def list(self, request):
return Response({}) return Response({})
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def total_resource(self, request): def total_resource(self, request):
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
data['label'].append(k) data["label"].append(k)
data['data'].append(v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS).count()) data["data"].append(
v.objects.filter(
invoice__state=Invoice.InvoiceState.IN_PROGRESS
).count()
)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def active_resource(self, request): def active_resource(self, request):
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
data['label'].append(k) data["label"].append(k)
data['data'].append( data["data"].append(
v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).count()) v.objects.filter(
invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None
).count()
)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def price_total_resource(self, request): def price_total_resource(self, request):
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
sum_of_price = sum([q.price_charged for q in sum_of_price = sum(
v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS).all()]) [
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) sum_of_price = sum_of_price or Money(
data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') amount=0, currency=settings.DEFAULT_CURRENCY
data['data'].append(sum_of_price.amount) )
data["label"].append(k + " (" + str(sum_of_price.currency) + ")")
data["data"].append(sum_of_price.amount)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def price_active_resource(self, request): def price_active_resource(self, request):
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
sum_of_price = sum([q.price_charged for q in sum_of_price = sum(
v.objects.filter(invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).all()]) [
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) sum_of_price = sum_of_price or Money(
data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') amount=0, currency=settings.DEFAULT_CURRENCY
data['data'].append(sum_of_price.amount) )
data["label"].append(k + " (" + str(sum_of_price.currency) + ")")
data["data"].append(sum_of_price.amount)
return Response(data) return Response(data)
@ -298,90 +452,113 @@ class ProjectOverviewViewSet(viewsets.ViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def get_tenant(self, request, pk): def get_tenant(self, request, pk):
project = BillingProject.objects.filter(tenant_id=pk).first() project = BillingProject.objects.filter(tenant_id=pk).first()
serializer = BillingProjectSerializer(project) serializer = BillingProjectSerializer(project)
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['POST']) @action(detail=True, methods=["POST"])
def update_email(self, request, pk): def update_email(self, request, pk):
project = BillingProject.objects.filter(tenant_id=pk).first() project = BillingProject.objects.filter(tenant_id=pk).first()
serializer = BillingProjectSerializer(project, data=request.data) serializer = BillingProjectSerializer(project, data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response({ return Response({"status": "success"})
"status": "success"
})
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def total_resource(self, request): 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() project = BillingProject.objects.filter(tenant_id=tenant_id).first()
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
data['label'].append(k) data["label"].append(k)
data['data'].append( data["data"].append(
v.objects.filter(invoice__project=project, invoice__state=Invoice.InvoiceState.IN_PROGRESS).count()) v.objects.filter(
invoice__project=project,
invoice__state=Invoice.InvoiceState.IN_PROGRESS,
).count()
)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def active_resource(self, request): 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() project = BillingProject.objects.filter(tenant_id=tenant_id).first()
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
data['label'].append(k) data["label"].append(k)
data['data'].append( data["data"].append(
v.objects.filter(invoice__project=project, invoice__state=Invoice.InvoiceState.IN_PROGRESS, v.objects.filter(
end_date=None).count()) invoice__project=project,
invoice__state=Invoice.InvoiceState.IN_PROGRESS,
end_date=None,
).count()
)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def price_total_resource(self, request): 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() project = BillingProject.objects.filter(tenant_id=tenant_id).first()
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
sum_of_price = sum([q.price_charged for q in sum_of_price = sum(
v.objects.filter(invoice__project=project, [
invoice__state=Invoice.InvoiceState.IN_PROGRESS).all()]) 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) sum_of_price = sum_of_price or Money(
data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') amount=0, currency=settings.DEFAULT_CURRENCY
data['data'].append(sum_of_price.amount) )
data["label"].append(k + " (" + str(sum_of_price.currency) + ")")
data["data"].append(sum_of_price.amount)
return Response(data) return Response(data)
@action(detail=False, methods=['GET']) @action(detail=False, methods=["GET"])
def price_active_resource(self, request): 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() project = BillingProject.objects.filter(tenant_id=tenant_id).first()
data = { data = {
'label': [], "label": [],
'data': [], "data": [],
} }
for k, v in INVOICE_COMPONENT_MODEL.items(): for k, v in INVOICE_COMPONENT_MODEL.items():
sum_of_price = sum([q.price_charged for q in sum_of_price = sum(
v.objects.filter(invoice__project=project, [
invoice__state=Invoice.InvoiceState.IN_PROGRESS, end_date=None).all()]) 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) sum_of_price = sum_of_price or Money(
data['label'].append(k + ' (' + str(sum_of_price.currency) + ')') amount=0, currency=settings.DEFAULT_CURRENCY
data['data'].append(sum_of_price.amount) )
data["label"].append(k + " (" + str(sum_of_price.currency) + ")")
data["data"].append(sum_of_price.amount)
return Response(data) return Response(data)
@ -390,13 +567,15 @@ class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = NotificationSerializer serializer_class = NotificationSerializer
def get_queryset(self): 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: if tenant_id is None:
return Notification.objects.order_by('-created_at') return Notification.objects.order_by("-created_at")
if tenant_id == '0': if tenant_id == "0":
return Notification.objects.filter(project=None).order_by('-created_at') 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): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@ -405,7 +584,7 @@ class NotificationViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def resend(self, request, pk): def resend(self, request, pk):
notification = Notification.objects.filter(id=pk).first() notification = Notification.objects.filter(id=pk).first()
notification.send() notification.send()
@ -414,7 +593,7 @@ class NotificationViewSet(viewsets.ModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def set_read(self, request, pk): def set_read(self, request, pk):
notification = Notification.objects.filter(id=pk).first() notification = Notification.objects.filter(id=pk).first()
notification.is_read = True notification.is_read = True
@ -424,7 +603,7 @@ class NotificationViewSet(viewsets.ModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def set_unread(self, request, pk): def set_unread(self, request, pk):
notification = Notification.objects.filter(id=pk).first() notification = Notification.objects.filter(id=pk).first()
notification.is_read = False notification.is_read = False
@ -447,34 +626,43 @@ class BalanceViewSet(viewsets.ViewSet):
balance = Balance.objects.filter(pk).first() balance = Balance.objects.filter(pk).first()
return Response(BalanceSerializer(balance).data) return Response(BalanceSerializer(balance).data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def retrieve_by_project(self, request, pk): def retrieve_by_project(self, request, pk):
project = get_object_or_404(BillingProject, tenant_id=pk) project = get_object_or_404(BillingProject, tenant_id=pk)
balance = Balance.get_balance_for_project(project=project) balance = Balance.get_balance_for_project(project=project)
return Response(BalanceSerializer(balance).data) return Response(BalanceSerializer(balance).data)
@action(detail=True, methods=['GET']) @action(detail=True, methods=["GET"])
def transaction_by_project(self, request, pk): def transaction_by_project(self, request, pk):
project = get_object_or_404(BillingProject, tenant_id=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) 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): def top_up_by_project(self, request, pk):
project = get_object_or_404(BillingProject, tenant_id=pk) project = get_object_or_404(BillingProject, tenant_id=pk)
balance = Balance.get_balance_for_project(project=project) balance = Balance.get_balance_for_project(project=project)
balance_transaction = balance.top_up( balance_transaction = balance.top_up(
Money(amount=request.data['amount'], currency=request.data['amount_currency']), Money(
description=request.data['description']) amount=request.data["amount"], currency=request.data["amount_currency"]
),
description=request.data["description"],
)
return Response(BalanceTransactionSerializer(balance_transaction).data) return Response(BalanceTransactionSerializer(balance_transaction).data)
@action(detail=True, methods=['POST']) @action(detail=True, methods=["POST"])
def top_down_by_project(self, request, pk): def top_down_by_project(self, request, pk):
project = get_object_or_404(BillingProject, tenant_id=pk) project = get_object_or_404(BillingProject, tenant_id=pk)
balance = Balance.get_balance_for_project(project=project) balance = Balance.get_balance_for_project(project=project)
balance_transaction = balance.top_down( balance_transaction = balance.top_down(
Money(amount=request.data['amount'], currency=request.data['amount_currency']), Money(
description=request.data['description']) amount=request.data["amount"], currency=request.data["amount_currency"]
),
description=request.data["description"],
)
return Response(BalanceTransactionSerializer(balance_transaction).data) return Response(BalanceTransactionSerializer(balance_transaction).data)
# endregion # endregion

30
core/metric.py Normal file
View file

@ -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'])

View file

@ -3,6 +3,7 @@ import math
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from djmoney.models.fields import MoneyField from djmoney.models.fields import MoneyField
from django.utils.timesince import timesince
class BaseModel(models.Model): class BaseModel(models.Model):
@ -70,6 +71,11 @@ class InvoiceComponentMixin(TimestampMixin, PriceMixin):
return self.hourly_price * hour_passes 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): def is_closed(self):
""" """
Is component closed Is component closed

View file

@ -9,3 +9,4 @@ djangorestframework==3.12.4
django-filter==2.4.0 django-filter==2.4.0
Markdown==3.3.4 Markdown==3.3.4
gunicorn==20.1.0 gunicorn==20.1.0
prometheus-client==0.16.0

View file

@ -15,8 +15,10 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from api import views
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('api.urls')) path('api/', include('api.urls')),
path('metrics', views.metrics)
] ]