-
-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feature] Added optional cleaninsights metric collection #360
Closes #360
- Loading branch information
Showing
18 changed files
with
930 additions
and
7 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
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,42 @@ | ||
from django.apps import AppConfig | ||
from django.conf import settings | ||
from django.db.models.signals import post_migrate | ||
|
||
|
||
class MeasurementsConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'openwisp_utils.measurements' | ||
app_label = 'openwisp_measurements' | ||
|
||
def ready(self): | ||
super().ready() | ||
self.connect_post_migrate_signal() | ||
|
||
def connect_post_migrate_signal(self): | ||
post_migrate.connect(self.post_migrate_receiver, sender=self) | ||
|
||
@classmethod | ||
def post_migrate_receiver(cls, **kwargs): | ||
if getattr(settings, 'DEBUG', False): | ||
# Do not send usage metrics in debug mode | ||
# i.e. when running tests. | ||
return | ||
|
||
from .tasks import send_usage_metrics | ||
|
||
is_new_install = False | ||
if kwargs.get('plan'): | ||
migration, migration_rolled_back = kwargs['plan'][0] | ||
is_new_install = ( | ||
migration_rolled_back is False | ||
and str(migration) == 'contenttypes.0001_initial' | ||
) | ||
|
||
# If the migration plan includes creating table | ||
# for the ContentType model, then the installation is | ||
# treated as a new installation. | ||
if is_new_install: | ||
# This is a new installation | ||
send_usage_metrics.delay() | ||
else: | ||
send_usage_metrics.delay(upgrade_only=True) |
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,42 @@ | ||
# Generated by Django 4.2.7 on 2023-12-06 15:30 | ||
|
||
from django.db import migrations, models | ||
import django.utils.timezone | ||
import model_utils.fields | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="OpenwispVersion", | ||
fields=[ | ||
( | ||
"id", | ||
models.UUIDField( | ||
default=uuid.uuid4, | ||
editable=False, | ||
primary_key=True, | ||
serialize=False, | ||
), | ||
), | ||
( | ||
"created", | ||
model_utils.fields.AutoCreatedField( | ||
default=django.utils.timezone.now, | ||
editable=False, | ||
verbose_name="created", | ||
), | ||
), | ||
("module_version", models.JSONField(blank=True, default=dict)), | ||
], | ||
options={ | ||
"ordering": ("-created",), | ||
}, | ||
), | ||
] |
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,46 @@ | ||
from django.db import models | ||
from openwisp_utils.base import TimeStampedEditableModel | ||
from packaging.version import parse as parse_version | ||
|
||
|
||
class OpenwispVersion(TimeStampedEditableModel): | ||
modified = None | ||
module_version = models.JSONField(default=dict, blank=True) | ||
|
||
class Meta: | ||
ordering = ('-created',) | ||
|
||
@classmethod | ||
def is_new_installation(cls): | ||
return not cls.objects.exists() | ||
|
||
@classmethod | ||
def get_upgraded_modules(cls, current_versions): | ||
""" | ||
Retrieves a dictionary of upgraded modules based on current versions. | ||
Also updates the OpenwispVersion object with the new versions. | ||
Args: | ||
current_versions (dict): A dictionary containing the current versions of modules. | ||
Returns: | ||
dict: A dictionary containing the upgraded modules and their versions. | ||
""" | ||
openwisp_version = cls.objects.first() | ||
if not openwisp_version: | ||
cls.objects.create(module_version=current_versions) | ||
return {} | ||
old_versions = openwisp_version.module_version | ||
upgraded_modules = {} | ||
for module, version in current_versions.items(): | ||
if module in old_versions and parse_version( | ||
old_versions[module] | ||
) < parse_version(version): | ||
upgraded_modules[module] = version | ||
openwisp_version.module_version[module] = version | ||
if upgraded_modules: | ||
# Save the new versions in a new object | ||
OpenwispVersion.objects.create( | ||
module_version=openwisp_version.module_version | ||
) | ||
return upgraded_modules |
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,56 @@ | ||
import logging | ||
|
||
from celery import shared_task | ||
from openwisp_utils.admin_theme.system_info import ( | ||
get_enabled_openwisp_modules, | ||
get_openwisp_version, | ||
get_os_details, | ||
) | ||
|
||
from ..tasks import OpenwispCeleryTask | ||
from ..utils import retryable_request | ||
from .models import OpenwispVersion | ||
from .utils import _get_events, get_openwisp_module_metrics, get_os_detail_metrics | ||
|
||
USER_METRIC_COLLECTION_URL = 'https://analytics.openwisp.io/cleaninsights.php' | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def post_usage_metrics(events): | ||
try: | ||
response = retryable_request( | ||
'post', | ||
url=USER_METRIC_COLLECTION_URL, | ||
json={ | ||
'idsite': 5, | ||
'events': events, | ||
}, | ||
max_retries=10, | ||
) | ||
assert response.status_code == 204 | ||
except Exception as error: | ||
if isinstance(error, AssertionError): | ||
message = f'HTTP {response.status_code} Response' | ||
else: | ||
message = str(error) | ||
logger.error( | ||
f'Collection of usage metrics failed, max retries exceeded. Error: {message}' | ||
) | ||
|
||
|
||
@shared_task(base=OpenwispCeleryTask) | ||
def send_usage_metrics(upgrade_only=False): | ||
current_versions = get_enabled_openwisp_modules() | ||
current_versions.update({'OpenWISP Version': get_openwisp_version()}) | ||
metrics = [] | ||
metrics.extend(get_os_detail_metrics(get_os_details())) | ||
if OpenwispVersion.is_new_installation(): | ||
metrics.extend(_get_events('Install', current_versions)) | ||
OpenwispVersion.objects.create(module_version=current_versions) | ||
else: | ||
upgraded_modules = OpenwispVersion.get_upgraded_modules(current_versions) | ||
metrics.extend(_get_events('Upgrade', upgraded_modules)) | ||
if not upgrade_only: | ||
metrics.extend(get_openwisp_module_metrics(current_versions)) | ||
post_usage_metrics(metrics) |
Oops, something went wrong.