Skip to content

Commit 0ad734e

Browse files
authored
Merge pull request #267 from membermatters/feature/more-prometheus-metrics
Added stats and metrics page to member portal
2 parents 60cb2d4 + c56c8b9 commit 0ad734e

31 files changed

+927
-319
lines changed

memberportal/api_general/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def get(self, request):
6060
"senderId": config.SMS_SENDER_ID,
6161
"footer": config.SMS_FOOTER,
6262
},
63+
"enableStatsPage": config.ENABLE_STATS_PAGE,
6364
}
6465

6566
keys = {"stripePublishableKey": config.STRIPE_PUBLISHABLE_KEY}

memberportal/api_member_bucks/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@
2222
views.MemberBucksDonateFunds.as_view(),
2323
name="MemberBucksDonateFunds",
2424
),
25+
path(
26+
"api/memberbucks/balance-list/",
27+
views.GetMemberbucksBalanceList.as_view(),
28+
name="GetMemberbucksBalanceList",
29+
),
2530
]

memberportal/api_member_bucks/views.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from django.db.models import Sum
2+
from rest_framework_api_key.permissions import HasAPIKey
3+
4+
from profile.models import Profile
15
from memberbucks.models import MemberBucks
26
from rest_framework import status, permissions
37
from rest_framework.response import Response
@@ -139,3 +143,28 @@ def post(self, request, amount=None):
139143
),
140144
)
141145
return Response()
146+
147+
148+
class GetMemberbucksBalanceList(APIView):
149+
"""
150+
get: This method returns the balance for every member in the system and the total memberbucks in circulation.
151+
"""
152+
153+
permission_classes = (permissions.IsAdminUser | HasAPIKey,)
154+
155+
def get(self, request):
156+
total_balance = Profile.objects.filter(memberbucks_balance__lt=1000).aggregate(
157+
Sum("memberbucks_balance")
158+
)
159+
member_balances = (
160+
Profile.objects.all()
161+
.order_by("-memberbucks_balance")
162+
.values("first_name", "last_name", "screen_name", "memberbucks_balance")
163+
)
164+
return Response(
165+
{
166+
"total_memberbucks": total_balance["memberbucks_balance__sum"],
167+
"member_balances": member_balances,
168+
},
169+
status=status.HTTP_200_OK,
170+
)

memberportal/api_metrics/metrics.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ def calculate_member_count():
5757
profile_states.append({"state": state["state"], "total": state["count"]})
5858

5959
Metric.objects.create(
60-
name=Metric.MetricName.MEMBER_COUNT_TOTAL, data=profile_states
60+
name=Metric.MetricName.MEMBER_COUNT_TOTAL,
61+
data=(
62+
profile_states if len(profile_states) else [{"state": "active", "total": 0}]
63+
),
6164
).full_clean()
6265

6366

@@ -74,7 +77,10 @@ def calculate_member_count_6_months():
7477
profile_states.append({"state": state["state"], "total": state["count"]})
7578

7679
Metric.objects.create(
77-
name=Metric.MetricName.MEMBER_COUNT_6_MONTHS, data=profile_states
80+
name=Metric.MetricName.MEMBER_COUNT_6_MONTHS,
81+
data=(
82+
profile_states if len(profile_states) else [{"state": "active", "total": 0}]
83+
),
7884
).full_clean()
7985

8086

@@ -91,7 +97,10 @@ def calculate_member_count_12_months():
9197
profile_states.append({"state": state["state"], "total": state["count"]})
9298

9399
Metric.objects.create(
94-
name=Metric.MetricName.MEMBER_COUNT_12_MONTHS, data=profile_states
100+
name=Metric.MetricName.MEMBER_COUNT_12_MONTHS,
101+
data=(
102+
profile_states if len(profile_states) else [{"state": "active", "total": 0}]
103+
),
95104
).full_clean()
96105

97106

@@ -109,13 +118,19 @@ def calculate_subscription_count():
109118
)
110119
Metric.objects.create(
111120
name=Metric.MetricName.SUBSCRIPTION_COUNT_TOTAL,
112-
data=subscription_states_data,
121+
data=(
122+
subscription_states_data
123+
if len(subscription_states_data)
124+
else [{"state": "inactive", "total": 0}]
125+
),
113126
).full_clean()
114127

115128

116129
def calculate_memberbucks_balance():
117130
logger.debug("Calculating memberbucks balance total")
118-
total_balance = Profile.objects.aggregate(Sum("memberbucks_balance"))
131+
total_balance = Profile.objects.filter(memberbucks_balance__lt=1000).aggregate(
132+
Sum("memberbucks_balance")
133+
)
119134
Metric.objects.create(
120135
name=Metric.MetricName.MEMBERBUCKS_BALANCE_TOTAL,
121136
data={"value": total_balance["memberbucks_balance__sum"]},
@@ -127,7 +142,8 @@ def calculate_memberbucks_transactions():
127142
logger.debug("Calculating subscription count total")
128143
transaction_data = []
129144
for transaction_type in (
130-
MemberBucks.objects.values("transaction_type")
145+
MemberBucks.objects.filter(amount__lt=1000)
146+
.values("transaction_type")
131147
.annotate(amount=Sum("amount"))
132148
.order_by("-amount")
133149
):
@@ -139,5 +155,9 @@ def calculate_memberbucks_transactions():
139155
)
140156
Metric.objects.create(
141157
name=Metric.MetricName.MEMBERBUCKS_TRANSACTIONS_TOTAL,
142-
data=transaction_data,
158+
data=(
159+
transaction_data
160+
if len(transaction_data)
161+
else [{"type": "stripe", "total": 0.0}]
162+
),
143163
).full_clean()

memberportal/api_metrics/tasks.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@
1010

1111
@app.on_after_finalize.connect
1212
def setup_periodic_tasks(sender, **kwargs):
13-
sender.add_periodic_task(
14-
config.METRICS_INTERVAL,
15-
calculate_metrics.s(),
16-
expires=60,
17-
name="celery_calculate_metrics",
18-
)
13+
if config.METRICS_INTERVAL:
14+
metrics_interval = config.METRICS_INTERVAL
15+
if metrics_interval < 3600 * 24:
16+
logger.warning(
17+
"METRICS_INTERVAL is less than 24 hours. This is NOT recommended for production and has little benefit."
18+
)
19+
if metrics_interval < 60:
20+
logger.warning(
21+
"METRICS_INTERVAL is less than 60 seconds, setting to 60 seconds."
22+
)
23+
metrics_interval = 60
24+
sender.add_periodic_task(
25+
metrics_interval,
26+
calculate_metrics.s(),
27+
expires=60,
28+
name="celery_calculate_metrics",
29+
)
1930

2031

2132
@app.task

memberportal/api_metrics/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@
1313
views.UpdatePromMetrics.as_view(),
1414
name="api_update_prom_metrics",
1515
),
16+
path(
17+
"api/update-statistics/",
18+
views.UpdateStatistics.as_view(),
19+
name="api_update_statistics",
20+
),
1621
]

memberportal/api_metrics/views.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import api_metrics.metrics
1+
from django.db.models import Max, Sum
2+
from django.utils import timezone
3+
from rest_framework_api_key.permissions import HasAPIKey
4+
5+
import api_metrics.metrics as api_metrics
26
from api_metrics.models import Metric
37
from api_general.models import SiteSession
48

9+
from constance import config
510
from rest_framework.response import Response
611
from rest_framework.views import APIView
712
from rest_framework import permissions
@@ -16,17 +21,67 @@ class Statistics(APIView):
1621
"""
1722

1823
def get(self, request):
24+
statistics = {}
25+
26+
# On site members
27+
on_site = {"members": [], "count": 0}
1928
members = SiteSession.objects.filter(signout_date=None).order_by("-signin_date")
20-
member_list = []
29+
on_site["count"] = members.count()
2130

2231
for member in members:
23-
member_list.append(member.user.profile.get_full_name())
24-
25-
statistics = {"onSite": {"members": member_list, "count": members.count()}}
32+
on_site["members"].append(member.user.profile.get_full_name())
33+
34+
statistics["on_site"] = on_site
35+
36+
for metric_name in Metric.MetricName.values:
37+
# Don't return any data from the API if the stats page isn't enabled
38+
if config.ENABLE_STATS_PAGE or request.user.is_admin:
39+
metric_data = []
40+
one_year_before_today = timezone.now() - timezone.timedelta(
41+
days=config.STATS_MAX_DAYS
42+
)
43+
last_stat_per_day = (
44+
Metric.objects.filter(
45+
name=metric_name, creation_date__gte=one_year_before_today
46+
)
47+
.extra(select={"the_date": "date(creation_date)"})
48+
.values_list("the_date")
49+
.order_by("-the_date")
50+
.annotate(max_date=Max("creation_date"))
51+
)
52+
max_dates = [item[1] for item in last_stat_per_day]
53+
metrics = Metric.objects.filter(
54+
name=metric_name, creation_date__in=max_dates
55+
).order_by("creation_date")
56+
for metric in metrics:
57+
metric_data.append(
58+
{"date": metric.creation_date, "data": metric.data}
59+
)
60+
statistics[metric_name] = metric_data
61+
else:
62+
statistics[metric_name] = []
2663

2764
return Response(statistics)
2865

2966

67+
class UpdateStatistics(APIView):
68+
"""
69+
put: This method updates and stores a new set of statistics.
70+
"""
71+
72+
permission_classes = (permissions.IsAdminUser | HasAPIKey,)
73+
74+
def put(self, request):
75+
api_metrics.calculate_member_count()
76+
api_metrics.calculate_member_count_6_months()
77+
api_metrics.calculate_member_count_12_months()
78+
api_metrics.calculate_subscription_count()
79+
api_metrics.calculate_memberbucks_balance()
80+
api_metrics.calculate_memberbucks_transactions()
81+
82+
return Response()
83+
84+
3085
class UpdatePromMetrics(APIView):
3186
"""
3287
post: triggers Django to update the Prometheus site metrics from the database.

memberportal/membermatters/constance_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@
334334
3600,
335335
"The interval in seconds to calculate and store application level metrics data like member count and door swipes.",
336336
),
337+
"ENABLE_STATS_PAGE": (
338+
True,
339+
"Enable the stats page that shows member counts and other metrics.",
340+
),
341+
"STATS_MAX_DAYS": (
342+
365,
343+
"The maximum number of days to show on the stats page.",
344+
),
337345
}
338346

339347
CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
@@ -366,6 +374,7 @@
366374
"ENABLE_DOOR_BUMP_API",
367375
),
368376
),
377+
("Stats Settings", ("ENABLE_STATS_PAGE", "STATS_MAX_DAYS")),
369378
(
370379
"Sentry Error Reporting",
371380
(

0 commit comments

Comments
 (0)