-
Notifications
You must be signed in to change notification settings - Fork 412
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Kim Gustyr <kim.gustyr@flagsmith.com>
- Loading branch information
1 parent
57f8d68
commit 5c25c41
Showing
28 changed files
with
874 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from core.models import AbstractBaseAuditableModel | ||
|
||
from audit.models import AuditLog | ||
from audit.related_object_type import RelatedObjectType | ||
from features.models import Feature, FeatureState | ||
from features.versioning.models import EnvironmentFeatureVersion | ||
|
||
|
||
def get_audited_instance_from_audit_log_record( | ||
audit_log_record: AuditLog, | ||
) -> AbstractBaseAuditableModel | None: | ||
""" | ||
Given an `AuditLog` model instance, return a model instance that produced the log. | ||
""" | ||
# There's currently four (sigh) ways an audit log record is created: | ||
# 1. Historical record | ||
# 2. Segment priorities changed | ||
# 3. Change request | ||
# 4. Environment feature version published | ||
|
||
# Try getting the historical record first. | ||
if history_record := audit_log_record.history_record: | ||
return history_record.instance | ||
|
||
# Try to infer the model class from `AuditLog.related_object_type`. | ||
match audit_log_record.related_object_type: | ||
# Assume segment priorities change. | ||
case RelatedObjectType.FEATURE.name: | ||
return ( | ||
Feature.objects.all_with_deleted() | ||
.filter( | ||
pk=audit_log_record.related_object_id, | ||
project=audit_log_record.project, | ||
) | ||
.first() | ||
) | ||
|
||
# Assume change request. | ||
case RelatedObjectType.FEATURE_STATE.name: | ||
return ( | ||
FeatureState.objects.all_with_deleted() | ||
.filter( | ||
pk=audit_log_record.related_object_id, | ||
environment=audit_log_record.environment, | ||
) | ||
.first() | ||
) | ||
|
||
# Assume environment feature version. | ||
case RelatedObjectType.EF_VERSION.name: | ||
return ( | ||
EnvironmentFeatureVersion.objects.all_with_deleted() | ||
.filter( | ||
uuid=audit_log_record.related_object_uuid, | ||
environment=audit_log_record.environment, | ||
) | ||
.first() | ||
) | ||
|
||
# All known audit log sources exhausted by now. | ||
# Since `RelatedObjectType` is not a 1:1 mapping to a model class, | ||
# generalised heuristics might be dangerous. | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
default_app_config = "integrations.grafana.apps.GrafanaConfigurationConfig" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
from django.apps import AppConfig | ||
|
||
|
||
class GrafanaConfigurationConfig(AppConfig): | ||
name = "integrations.grafana" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import json | ||
import logging | ||
from typing import Any | ||
|
||
import requests | ||
|
||
from audit.models import AuditLog | ||
from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper | ||
from integrations.grafana.mappers import ( | ||
map_audit_log_record_to_grafana_annotation, | ||
) | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
ROUTE_API_ANNOTATIONS = "/api/annotations" | ||
|
||
|
||
class GrafanaWrapper(AbstractBaseEventIntegrationWrapper): | ||
def __init__(self, base_url: str, api_key: str) -> None: | ||
base_url = base_url[:-1] if base_url.endswith("/") else base_url | ||
self.url = f"{base_url}{ROUTE_API_ANNOTATIONS}" | ||
self.api_key = api_key | ||
|
||
@staticmethod | ||
def generate_event_data(audit_log_record: AuditLog) -> dict[str, Any]: | ||
return map_audit_log_record_to_grafana_annotation(audit_log_record) | ||
|
||
def _headers(self) -> dict[str, str]: | ||
return { | ||
"Content-Type": "application/json", | ||
"Authorization": f"Bearer {self.api_key}", | ||
} | ||
|
||
def _track_event(self, event: dict[str, Any]) -> None: | ||
response = requests.post( | ||
url=self.url, | ||
headers=self._headers(), | ||
data=json.dumps(event), | ||
) | ||
|
||
logger.debug( | ||
"Sent event to Grafana. Response code was %s" % response.status_code | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
from audit.models import AuditLog | ||
from audit.services import get_audited_instance_from_audit_log_record | ||
from features.models import ( | ||
Feature, | ||
FeatureSegment, | ||
FeatureState, | ||
FeatureStateValue, | ||
) | ||
from integrations.grafana.types import GrafanaAnnotation | ||
from segments.models import Segment | ||
|
||
|
||
def _get_feature_tags( | ||
feature: Feature, | ||
) -> list[str]: | ||
return list(feature.tags.values_list("label", flat=True)) | ||
|
||
|
||
def _get_instance_tags_from_audit_log_record( | ||
audit_log_record: AuditLog, | ||
) -> list[str]: | ||
if instance := get_audited_instance_from_audit_log_record(audit_log_record): | ||
if isinstance(instance, Feature): | ||
return [ | ||
f"feature:{instance.name}", | ||
*_get_feature_tags(instance), | ||
] | ||
|
||
if isinstance(instance, FeatureState): | ||
return [ | ||
f"feature:{(feature := instance.feature).name}", | ||
f'flag:{"enabled" if instance.enabled else "disabled"}', | ||
*_get_feature_tags(feature), | ||
] | ||
|
||
if isinstance(instance, FeatureStateValue): | ||
return [ | ||
f"feature:{(feature := instance.feature_state.feature).name}", | ||
*_get_feature_tags(feature), | ||
] | ||
|
||
if isinstance(instance, Segment): | ||
return [f"segment:{instance.name}"] | ||
|
||
if isinstance(instance, FeatureSegment): | ||
return [ | ||
f"feature:{(feature := instance.feature).name}", | ||
f"segment:{instance.segment.name}", | ||
*_get_feature_tags(feature), | ||
] | ||
|
||
return [] | ||
|
||
|
||
def map_audit_log_record_to_grafana_annotation( | ||
audit_log_record: AuditLog, | ||
) -> GrafanaAnnotation: | ||
tags = [ | ||
"flagsmith", | ||
f"project:{audit_log_record.project_name}", | ||
f"environment:{audit_log_record.environment_name}", | ||
f"by:{audit_log_record.author_identifier}", | ||
*_get_instance_tags_from_audit_log_record(audit_log_record), | ||
] | ||
time = int(audit_log_record.created_date.timestamp() * 1000) # ms since epoch | ||
|
||
return { | ||
"tags": tags, | ||
"text": audit_log_record.log, | ||
"time": time, | ||
"timeEnd": time, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Generated by Django 3.2.25 on 2024-06-21 20:31 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import django_lifecycle.mixins | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
('environments', '0034_alter_environment_project'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='GrafanaConfiguration', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('deleted_at', models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True)), | ||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), | ||
('api_key', models.CharField(max_length=100)), | ||
('base_url', models.URLField(null=True)), | ||
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='grafana_config', to='projects.Project')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import logging | ||
|
||
from django.db import models | ||
|
||
from integrations.common.models import IntegrationsModel | ||
from projects.models import Project | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class GrafanaConfiguration(IntegrationsModel): | ||
""" | ||
Example `integration_data` entry: | ||
``` | ||
"grafana": { | ||
"perEnvironment": false, | ||
"image": "/static/images/integrations/grafana.svg", | ||
"docs": "https://docs.flagsmith.com/integrations/apm/grafana", | ||
"fields": [ | ||
{ | ||
"key": "base_url", | ||
"label": "Base URL", | ||
"default": "https://grafana.com" | ||
}, | ||
{ | ||
"key": "api_key", | ||
"label": "Service account token", | ||
"hidden": true | ||
} | ||
], | ||
"tags": [ | ||
"logging" | ||
], | ||
"title": "Grafana", | ||
"description": "Receive Flagsmith annotations to your Grafana instance on feature flag and segment changes." | ||
}, | ||
``` | ||
""" | ||
|
||
base_url = models.URLField(blank=False, null=True) | ||
project = models.OneToOneField( | ||
Project, on_delete=models.CASCADE, related_name="grafana_config" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from integrations.common.serializers import ( | ||
BaseProjectIntegrationModelSerializer, | ||
) | ||
|
||
from .models import GrafanaConfiguration | ||
|
||
|
||
class GrafanaConfigurationSerializer(BaseProjectIntegrationModelSerializer): | ||
class Meta: | ||
model = GrafanaConfiguration | ||
fields = ("id", "base_url", "api_key") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from typing import TypedDict | ||
|
||
|
||
class GrafanaAnnotation(TypedDict): | ||
tags: list[str] | ||
text: str | ||
time: int | ||
timeEnd: int |
Oops, something went wrong.