Skip to content

Commit 916724a

Browse files
committed
wip
1 parent 496220a commit 916724a

24 files changed

Lines changed: 912 additions & 541 deletions
Lines changed: 121 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from datetime import date, datetime, timedelta
2+
from logging import getLogger
23
from typing import TYPE_CHECKING, List
34

45
from dateutil.relativedelta import relativedelta
56
from django.conf import settings
6-
from django.db.models import Sum
7+
from django.db.models import Q, Sum
78
from django.utils import timezone
89
from rest_framework.exceptions import NotFound
910

11+
from app_analytics import constants
1012
from app_analytics.dataclasses import FeatureEvaluationData, UsageData
1113
from app_analytics.influxdb_wrapper import (
1214
get_events_for_organisation,
@@ -17,86 +19,57 @@
1719
from app_analytics.influxdb_wrapper import (
1820
get_usage_data as get_usage_data_from_influxdb,
1921
)
22+
from app_analytics.mappers import map_annotated_api_usage_buckets_to_usage_data
2023
from app_analytics.models import (
2124
APIUsageBucket,
2225
FeatureEvaluationBucket,
23-
Resource,
2426
)
27+
from app_analytics.types import Labels, PeriodType
2528
from environments.models import Environment
2629
from features.models import Feature
27-
from organisations.models import Organisation
30+
from organisations.models import Organisation, OrganisationSubscriptionInformationCache
2831

29-
from . import constants
30-
from .types import PERIOD_TYPE
32+
logger = getLogger(__name__)
3133

3234

3335
def get_usage_data(
3436
organisation: Organisation,
3537
environment_id: int | None = None,
3638
project_id: int | None = None,
37-
period: PERIOD_TYPE | None = None,
39+
period: PeriodType | None = None,
40+
labels_filter: Labels | None = None,
3841
) -> list[UsageData]:
39-
now = timezone.now()
40-
date_start = date_stop = None
41-
sub_cache = getattr(organisation, "subscription_information_cache", None)
42+
sub_cache = OrganisationSubscriptionInformationCache.objects.filter(
43+
organisation=organisation
44+
).first()
4245

43-
is_subscription_valid = (
44-
sub_cache is not None and sub_cache.is_billing_terms_dates_set()
46+
date_start, date_stop = _get_start_date_and_stop_date_for_subscribed_organisation(
47+
sub_cache=sub_cache,
48+
period=period,
4549
)
4650

47-
if period in (constants.CURRENT_BILLING_PERIOD, constants.PREVIOUS_BILLING_PERIOD):
48-
if not is_subscription_valid:
49-
raise NotFound("No billing periods found for this organisation.")
50-
51-
if TYPE_CHECKING:
52-
assert sub_cache is not None
53-
54-
match period:
55-
case constants.CURRENT_BILLING_PERIOD:
56-
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
57-
days=30
58-
)
59-
month_delta = relativedelta(now, starts_at).months
60-
date_start = relativedelta(months=month_delta) + starts_at
61-
date_stop = now
62-
63-
case constants.PREVIOUS_BILLING_PERIOD:
64-
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
65-
days=30
66-
)
67-
month_delta = relativedelta(now, starts_at).months - 1
68-
month_delta += relativedelta(now, starts_at).years * 12
69-
date_start = relativedelta(months=month_delta) + starts_at
70-
date_stop = relativedelta(months=month_delta + 1) + starts_at
71-
case constants.NINETY_DAY_PERIOD:
72-
date_start = now - relativedelta(days=90)
73-
date_stop = now
74-
7551
if settings.USE_POSTGRES_FOR_ANALYTICS:
76-
kwargs = {
77-
"organisation": organisation,
78-
"environment_id": environment_id,
79-
"project_id": project_id,
80-
}
81-
if date_start:
82-
assert date_stop
83-
kwargs["date_start"] = date_start # type: ignore[assignment]
84-
kwargs["date_stop"] = date_stop # type: ignore[assignment]
85-
86-
return get_usage_data_from_local_db(**kwargs) # type: ignore[arg-type]
87-
88-
kwargs = {
89-
"organisation_id": organisation.id,
90-
"environment_id": environment_id,
91-
"project_id": project_id,
92-
}
52+
return get_usage_data_from_local_db(
53+
organisation=organisation,
54+
environment_id=environment_id,
55+
project_id=project_id,
56+
date_start=date_start,
57+
date_stop=date_stop,
58+
labels_filter=labels_filter,
59+
)
9360

94-
if date_start:
95-
assert date_stop
96-
kwargs["date_start"] = date_start # type: ignore[assignment]
97-
kwargs["date_stop"] = date_stop # type: ignore[assignment]
61+
if settings.INFLUXDB_TOKEN:
62+
return get_usage_data_from_influxdb(
63+
organisation_id=organisation.id,
64+
environment_id=environment_id,
65+
project_id=project_id,
66+
date_start=date_start,
67+
date_stop=date_stop,
68+
labels_filter=labels_filter,
69+
)
9870

99-
return get_usage_data_from_influxdb(**kwargs) # type: ignore[arg-type]
71+
logger.warning(constants.NO_ANALYTICS_DATABASE_CONFIGURED_WARNING)
72+
return []
10073

10174

10275
def get_usage_data_from_local_db(
@@ -105,6 +78,7 @@ def get_usage_data_from_local_db(
10578
project_id: int | None = None,
10679
date_start: datetime | None = None,
10780
date_stop: datetime | None = None,
81+
labels_filter: Labels | None = None,
10882
) -> List[UsageData]:
10983
if date_start is None:
11084
date_start = timezone.now() - timedelta(days=30)
@@ -127,28 +101,20 @@ def get_usage_data_from_local_db(
127101
if environment_id:
128102
qs = qs.filter(environment_id=environment_id)
129103

104+
if labels_filter:
105+
qs = qs.filter(labels__contains=labels_filter)
106+
130107
qs = (
131108
qs.filter( # type: ignore[assignment]
132109
created_at__date__lte=date_stop,
133110
created_at__date__gt=date_start,
134111
)
135112
.order_by("created_at__date")
136-
.values("created_at__date", "resource")
113+
.values("created_at__date", "resource", "labels")
137114
.annotate(count=Sum("total_count"))
138115
)
139-
data_by_day = {}
140-
for row in qs: # TODO Write proper mappers for this?
141-
day = row["created_at__date"]
142-
if day not in data_by_day:
143-
data_by_day[day] = UsageData(day=day)
144-
if column_name := Resource(row["resource"]).column_name:
145-
setattr(
146-
data_by_day[day],
147-
column_name,
148-
row["count"],
149-
)
150116

151-
return data_by_day.values() # type: ignore[return-value]
117+
return map_annotated_api_usage_buckets_to_usage_data(qs)
152118

153119

154120
def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
@@ -168,30 +134,50 @@ def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
168134

169135

170136
def get_feature_evaluation_data(
171-
feature: Feature, environment_id: int, period: int = 30
137+
feature: Feature,
138+
environment_id: int,
139+
period: int = 30,
140+
labels_filter: Labels | None = None,
172141
) -> List[FeatureEvaluationData]:
173142
if settings.USE_POSTGRES_FOR_ANALYTICS:
174143
return get_feature_evaluation_data_from_local_db(
175-
feature, environment_id, period
144+
feature=feature,
145+
environment_id=environment_id,
146+
period=period,
147+
labels_filter=labels_filter,
148+
)
149+
150+
if settings.INFLUXDB_TOKEN:
151+
return get_feature_evaluation_data_from_influxdb(
152+
feature_name=feature.name,
153+
environment_id=environment_id,
154+
period=f"{period}d",
155+
labels_filter=labels_filter,
176156
)
177-
return get_feature_evaluation_data_from_influxdb(
178-
feature_name=feature.name, environment_id=environment_id, period=f"{period}d"
179-
)
157+
158+
logger.warning(constants.NO_ANALYTICS_DATABASE_CONFIGURED_WARNING)
159+
return []
180160

181161

182162
def get_feature_evaluation_data_from_local_db(
183-
feature: Feature, environment_id: int, period: int = 30
163+
feature: Feature,
164+
environment_id: int,
165+
period: int = 30,
166+
labels_filter: Labels | None = None,
184167
) -> List[FeatureEvaluationData]:
168+
filter = Q(
169+
environment_id=environment_id,
170+
bucket_size=constants.ANALYTICS_READ_BUCKET_SIZE,
171+
feature_name=feature.name,
172+
created_at__date__lte=timezone.now(),
173+
created_at__date__gt=timezone.now() - timedelta(days=period),
174+
)
175+
if labels_filter:
176+
filter &= Q(labels__contains=labels_filter)
185177
feature_evaluation_data = (
186-
FeatureEvaluationBucket.objects.filter(
187-
environment_id=environment_id,
188-
bucket_size=constants.ANALYTICS_READ_BUCKET_SIZE,
189-
feature_name=feature.name,
190-
created_at__date__lte=timezone.now(),
191-
created_at__date__gt=timezone.now() - timedelta(days=period),
192-
)
178+
FeatureEvaluationBucket.objects.filter(filter)
193179
.order_by("created_at__date")
194-
.values("created_at__date", "feature_name", "environment_id")
180+
.values("created_at__date", "feature_name", "environment_id", "labels")
195181
.annotate(count=Sum("total_count"))
196182
)
197183
usage_list = []
@@ -200,15 +186,61 @@ def get_feature_evaluation_data_from_local_db(
200186
FeatureEvaluationData(
201187
day=data["created_at__date"],
202188
count=data["count"],
189+
labels=data["labels"],
203190
)
204191
)
205192
return usage_list
206193

207194

208-
def _get_environment_ids_for_org(organisation) -> List[int]: # type: ignore[no-untyped-def]
195+
def _get_environment_ids_for_org(organisation: Organisation) -> list[int]:
209196
# We need to do this to prevent Django from generating a query that
210197
# references the environments and projects tables,
211198
# as they do not exist in the analytics database.
212199
return [
213200
e.id for e in Environment.objects.filter(project__organisation=organisation)
214201
]
202+
203+
204+
def _get_start_date_and_stop_date_for_subscribed_organisation(
205+
sub_cache: OrganisationSubscriptionInformationCache | None,
206+
period: PeriodType | None = None,
207+
) -> tuple[datetime | None, datetime | None]:
208+
"""
209+
Populate start and stop date for the given period
210+
from the organisation's subscription information.
211+
"""
212+
if period in {constants.CURRENT_BILLING_PERIOD, constants.PREVIOUS_BILLING_PERIOD}:
213+
if not (sub_cache and sub_cache.is_billing_terms_dates_set()):
214+
raise NotFound("No billing periods found for this organisation.")
215+
216+
if TYPE_CHECKING:
217+
assert sub_cache
218+
219+
now = timezone.now()
220+
221+
match period:
222+
case constants.CURRENT_BILLING_PERIOD:
223+
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
224+
days=30
225+
)
226+
month_delta = relativedelta(now, starts_at).months
227+
date_start = relativedelta(months=month_delta) + starts_at
228+
date_stop = now
229+
230+
case constants.PREVIOUS_BILLING_PERIOD:
231+
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
232+
days=30
233+
)
234+
month_delta = relativedelta(now, starts_at).months - 1
235+
month_delta += relativedelta(now, starts_at).years * 12
236+
date_start = relativedelta(months=month_delta) + starts_at
237+
date_stop = relativedelta(months=month_delta + 1) + starts_at
238+
239+
case constants.NINETY_DAY_PERIOD:
240+
date_start = now - relativedelta(days=90)
241+
date_stop = now
242+
243+
case _:
244+
return None, None
245+
246+
return date_start, date_stop

0 commit comments

Comments
 (0)