Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: API usage alerting in production #3507

Merged
merged 28 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f0c6406
WIP: use flagsmith to filter organisations to include in usage alerting
matthewelwell Feb 27, 2024
781c474
Move get client to outside of loop
zachaysan Feb 28, 2024
5989a60
Implement local evaluation for flagsmith client
zachaysan Feb 28, 2024
888022c
Fix influxdb query call
zachaysan Feb 28, 2024
3715eb3
Merge branch 'main' into fix/api-usage-alerting-in-production
zachaysan Feb 28, 2024
a2b75c5
Trigger build
zachaysan Feb 28, 2024
a5dc209
Trigger build
zachaysan Feb 28, 2024
c8b39f8
Update tests to pass CI
zachaysan Mar 1, 2024
761c372
Add comment with PR
zachaysan Mar 13, 2024
3947f6a
Use a local for easier debugging
zachaysan Mar 13, 2024
f52b9b3
Create test with feature flag being toggled off and add fine grain cl…
zachaysan Mar 13, 2024
c882d34
Merge branch 'main' into fix/api-usage-alerting-in-production
zachaysan Mar 13, 2024
833a17a
Remove temporary code
zachaysan Mar 14, 2024
8157896
Update dependencies
zachaysan Mar 15, 2024
268c46d
Trigger build
zachaysan Mar 15, 2024
6db904a
Trigger build
zachaysan Mar 15, 2024
32c7b06
Reduce whitespace to test CI
zachaysan Mar 15, 2024
0210383
Fix conflicts and merge branch 'main' into fix/api-usage-alerting-in-…
zachaysan Mar 15, 2024
352f299
Update due to library versioning
zachaysan Mar 15, 2024
031449f
Add new data since upgrade
zachaysan Mar 15, 2024
a69929c
Add composite key
zachaysan Mar 15, 2024
77b3e1d
Freeze time
zachaysan Mar 15, 2024
091b010
Set environment id to string for pydantic
zachaysan Mar 15, 2024
585710f
Add IdentityFeaturesList to test
zachaysan Mar 18, 2024
eedcc37
Avoid double iteration
zachaysan Mar 18, 2024
1f07ee1
Fix conflicts, add fixture, and merge branch 'main' into fix/api-usag…
zachaysan Mar 18, 2024
136bdd1
Merge branch 'fix/api-usage-alerting-in-production' of github.com:Fla…
zachaysan Mar 18, 2024
2d3b46c
Fix conflicts and merge branch 'main' into fix/api-usage-alerting-in-…
zachaysan Mar 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/app/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from drf_yasg import openapi
from drf_yasg.inspectors import PaginatorInspector
from flag_engine.identities.builders import build_identity_model
from flag_engine.identities.models import IdentityModel
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response

Expand Down Expand Up @@ -75,7 +75,7 @@ def paginate_queryset(self, dynamo_queryset, request, view=None):
)

return [
build_identity_model(identity_document)
IdentityModel.model_validate(identity_document)
for identity_document in dynamo_queryset["Items"]
]

Expand Down
5 changes: 2 additions & 3 deletions api/app_analytics/influxdb_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,7 @@ def get_current_api_usage(organisation_id: int, date_range: str) -> int:
),
drop_columns=("_start", "_stop", "_time"),
extra='|> sum() \
|> group() \
|> sort(columns: ["_value"], desc: true) ',
|> sort(columns: ["_value"], desc: true) ',
)

for result in results:
Expand All @@ -346,7 +345,7 @@ def get_current_api_usage(organisation_id: int, date_range: str) -> int:
return 0

# There should only be one matching result due to the
# group part of the query.
# sum part of the query.
assert len(result.records) == 1
return result.records[0].get_value()

Expand Down
7 changes: 6 additions & 1 deletion api/edge_api/identities/edge_identity_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ def get_edge_identity_overrides(
environment_id=environment_id, feature_id=feature_id
)
)
return [IdentityOverrideV2.parse_obj(item) for item in override_items]
return [
IdentityOverrideV2.model_validate(
{**item, "environment_id": str(item["environment_id"])}
)
for item in override_items
]
3 changes: 1 addition & 2 deletions api/edge_api/identities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from django.db.models import Prefetch, Q
from flag_engine.features.models import FeatureStateModel
from flag_engine.identities.builders import build_identity_model
from flag_engine.identities.models import IdentityFeaturesList, IdentityModel

from api_keys.models import MasterAPIKey
Expand Down Expand Up @@ -32,7 +31,7 @@ def __init__(self, engine_identity_model: IdentityModel):

@classmethod
def from_identity_document(cls, identity_document: dict) -> "EdgeIdentity":
return EdgeIdentity(build_identity_model(identity_document))
return EdgeIdentity(IdentityModel.model_validate(identity_document))

@property
def django_id(self) -> int:
Expand Down
6 changes: 3 additions & 3 deletions api/edge_api/identities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
from flag_engine.identities.builders import build_identity_model
from flag_engine.identities.models import IdentityModel
from flag_engine.identities.traits.models import TraitModel
from pyngo import drf_error_details
from rest_framework import status, viewsets
Expand Down Expand Up @@ -166,7 +166,7 @@ def perform_destroy(self, instance):
)
@action(detail=True, methods=["get"], url_path="list-traits")
def get_traits(self, request, *args, **kwargs):
identity = build_identity_model(self.get_object())
identity = IdentityModel.model_validate(self.get_object())
data = [trait.dict() for trait in identity.identity_traits]
return Response(data=data, status=status.HTTP_200_OK)

Expand All @@ -180,7 +180,7 @@ def update_traits(self, request, *args, **kwargs):
environment = self.get_environment_from_request()
if not environment.project.organisation.persist_trait_data:
raise TraitPersistenceError()
identity = build_identity_model(self.get_object())
identity = IdentityModel.model_validate(self.get_object())
try:
trait = TraitModel(**request.data)
except pydantic.ValidationError as validation_error:
Expand Down
7 changes: 3 additions & 4 deletions api/environments/dynamodb/wrappers/identity_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from boto3.dynamodb.conditions import Key
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from flag_engine.environments.builders import build_environment_model
from flag_engine.identities.builders import build_identity_model
from flag_engine.environments.models import EnvironmentModel
from flag_engine.identities.models import IdentityModel
from flag_engine.segments.evaluator import get_identity_segments
from rest_framework.exceptions import NotFound
Expand Down Expand Up @@ -153,11 +152,11 @@ def get_segment_ids(
raise ValueError("Must provide one of identity_pk or identity_model.")

with suppress(ObjectDoesNotExist):
identity = identity_model or build_identity_model(
identity = identity_model or IdentityModel.model_validate(
self.get_item_from_uuid(identity_pk)
)
environment_wrapper = DynamoEnvironmentWrapper()
environment = build_environment_model(
environment = EnvironmentModel.model_validate(
environment_wrapper.get_item(identity.environment_api_key)
)
segments = get_identity_segments(environment, identity)
Expand Down
18 changes: 10 additions & 8 deletions api/integrations/flagsmith/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
environment_flags = get_client().get_environment_flags()
identity_flags = get_client().get_identity_flags()
```

Possible extensions:
- Allow for multiple clients?
"""
import typing

Expand All @@ -21,14 +18,19 @@
from integrations.flagsmith.exceptions import FlagsmithIntegrationError
from integrations.flagsmith.flagsmith_service import ENVIRONMENT_JSON_PATH

_flagsmith_client: typing.Optional[Flagsmith] = None
_flagsmith_clients: dict[str, Flagsmith] = {}


def get_client() -> Flagsmith:
global _flagsmith_client
def get_client(name: str = "default", local_eval: bool = False) -> Flagsmith:
global _flagsmith_clients

if not _flagsmith_client:
_flagsmith_client = Flagsmith(**_get_client_kwargs())
try:
_flagsmith_client = _flagsmith_clients[name]
except (KeyError, TypeError):
kwargs = _get_client_kwargs()
kwargs["enable_local_evaluation"] = local_eval
_flagsmith_client = Flagsmith(**kwargs)
_flagsmith_clients[name] = _flagsmith_client

return _flagsmith_client

Expand Down
10 changes: 10 additions & 0 deletions api/organisations/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.template.loader import render_to_string
from django.utils import timezone

from integrations.flagsmith.client import get_client
from organisations import subscription_info_cache
from organisations.models import (
OranisationAPIUsageNotification,
Expand Down Expand Up @@ -164,12 +165,21 @@ def _handle_api_usage_notifications(organisation: Organisation):


def handle_api_usage_notifications():
flagsmith_client = get_client("local", local_eval=True)

for organisation in Organisation.objects.filter(
subscription_information_cache__current_billing_term_starts_at__isnull=False,
subscription_information_cache__current_billing_term_ends_at__isnull=False,
).select_related(
"subscription_information_cache",
):
feature_enabled = flagsmith_client.get_identity_flags(
f"org.{organisation.id}.{organisation.name}",
traits={"organisation_id": organisation.id},
).is_feature_enabled("api_usage_alerting")
if not feature_enabled:
continue

try:
_handle_api_usage_notifications(organisation)
except RuntimeError:
Expand Down
Loading
Loading