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

Initial attempt to add UI metrics to influx #1176

Merged
merged 15 commits into from
Sep 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions galaxy/api/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,22 @@ def can_delete(self, data):
return False


class InfluxSessionAccess(BaseAccess):
model = models.InfluxSessionIdentifier

def can_read(self, obj):
return True

def can_add(self, data):
return True

def can_change(self, obj, data):
return False

def can_delete(self, obj):
return False


register_access(EmailConfirmation, EmailConfirmationAccess)
register_access(User, UserAccess)
register_access(EmailAddress, EmailAddressAccess)
Expand All @@ -440,3 +456,4 @@ def can_delete(self, data):
register_access(models.ContentType, ContentTypeAccess)
register_access(models.CloudPlatform, CloudPlatformsAccess)
register_access(models.CommunitySurvey, CommunitySurveyAccess)
register_access(models.InfluxSessionIdentifier, InfluxSessionAccess)
1 change: 1 addition & 0 deletions galaxy/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
from .serializers import * # noqa
from .token import * # noqa
from .survey import * # noqa
from .influx import * # noqa
188 changes: 188 additions & 0 deletions galaxy/api/serializers/influx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# (c) 2012-2018, Ansible by Red Hat
#
# This file is part of Ansible Galaxy
#
# Ansible Galaxy is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by
# the Apache Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Ansible Galaxy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License
# along with Galaxy. If not, see <http://www.apache.org/licenses/>.

from . import serializers
from rest_framework import serializers as drf_serializers
from django.conf import settings

import influxdb

from galaxy.main import models

__all__ = (
'InfluxSessionSerializer',
'InfluxTypes',
)


class InfluxSessionSerializer(serializers.BaseSerializer):
class Meta:
model = models.InfluxSessionIdentifier
fields = (
'session_id',
)

def get_url(self, obj):
return ''

def get_summary_fields(self, instance):
return {}


####################################################
# Influx "Schema"
####################################################
# Influx doesn't have set schema's like most relational databases have, so it's
# up to the programmer to enforce some level of strcture to the data at the
# application level. Galaxy is currently using Django Rest Framework
# to verify that the shape of the data is correct before we insert it into
# influx. This means that the following serializers are roughly equivalent to
# models, and should be changed as little as possible to ensure data
# consistency.


# Influx data is organized into 'measurements' which are roughly equivalent to
# database tables. Each measurement contains at least one field and zero to
# many tags. Fields and tags are similar to columns with a few caveats.
class BaseMeasurement(drf_serializers.Serializer):
measurement = drf_serializers.CharField()

def save(self):
client = influxdb.InfluxDBClient(
host=settings.INFLUX_DB_HOST,
port=settings.INFLUX_DB_PORT,
username=settings.INFLUX_DB_USERNAME,
password=settings.INFLUX_DB_PASSWORD
)

client.switch_database(settings.INFLUX_DB_UI_EVENTS_DB_NAME)

try:
client.write_points([self.data])
except influxdb.client.InfluxDBClientError:
client.create_database(settings.INFLUX_DB_UI_EVENTS_DB_NAME)
client.switch_database(settings.INFLUX_DB_UI_EVENTS_DB_NAME)
client.write_points([self.data])


# Tags are indexed and only support string types. They should be used grouping
# data that will be queried frequently, but should not be used for variables
# that can have a large number of values (search keywords, repository names,
# sessions etc). Data can be grouped by tags, but the number of distinct tags
# cannot be easily counted.
class BaseTags(drf_serializers.Serializer):
current_component = drf_serializers.CharField(allow_blank=True)


# Fields are not indexed, but can be numeric or string types. Database
# functions such as DISTINCT, COUNT, SUM, AVERAGE etc can be used on fields.
class BaseFields(drf_serializers.Serializer):
session_id = drf_serializers.UUIDField()
current_page = drf_serializers.CharField()


# The influx library expects data to be shaped like so:
#
# {
# "measurement": "measurment_name",
# "tags": {
# "tag1": "value",
# },
# "fields": {
# "field1": 1
# }
# }

# Page Load
class PageLoadTags(BaseTags):
from_component = drf_serializers.CharField(allow_blank=True)


class PageLoadFields(BaseFields):
load_time = drf_serializers.FloatField()
from_page = drf_serializers.CharField()


class PageLoadMeasurementSerializer(BaseMeasurement):
tags = PageLoadTags()
fields = PageLoadFields()


# Search
class SearchQueryTags(BaseTags):
cloud_platforms = drf_serializers.CharField(
required=False, allow_blank=True
)
vendor = drf_serializers.BooleanField(required=False)
deprecated = drf_serializers.BooleanField(required=False)
content_type = drf_serializers.CharField(required=False, allow_blank=True)
platforms = drf_serializers.CharField(required=False, allow_blank=True)
tags = drf_serializers.CharField(required=False, allow_blank=True)
order_by = drf_serializers.CharField(required=False, allow_blank=True)


class SearchQueryFields(BaseFields):
keywords = drf_serializers.CharField(required=False, allow_blank=True)
namespaces = drf_serializers.CharField(required=False, allow_blank=True)
number_of_results = drf_serializers.IntegerField()


class SearchQueryMeasurementSerializer(BaseMeasurement):
tags = SearchQueryTags()
fields = SearchQueryFields()


class SearchLinkFields(BaseFields):
content_clicked = drf_serializers.CharField()
position_in_results = drf_serializers.IntegerField()
download_rank = drf_serializers.FloatField()
search_rank = drf_serializers.FloatField()
relevance = drf_serializers.FloatField()
keywords = drf_serializers.CharField(required=False, allow_blank=True)


class SearchLinkMeasurementSerializer(BaseMeasurement):
tags = SearchQueryTags()
fields = SearchLinkFields()


# UI Interactions
class ClickFields(BaseFields):
name = drf_serializers.CharField()


class LinkClickFields(ClickFields):
href = drf_serializers.CharField(allow_blank=True)


class ButtonClickMeasurementSerializer(BaseMeasurement):
tags = BaseTags()
fields = ClickFields()


class LinkClickMeasurementSerializer(BaseMeasurement):
tags = BaseTags()
fields = LinkClickFields()


InfluxTypes = {
'page_load': PageLoadMeasurementSerializer,
'search_query': SearchQueryMeasurementSerializer,
'search_click': SearchLinkMeasurementSerializer,
'button_click': ButtonClickMeasurementSerializer,
'link_click': LinkClickMeasurementSerializer
}
12 changes: 12 additions & 0 deletions galaxy/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

from galaxy.api import views

event_tracking_urls = [
url(r'^influx_session/$',
views.InfluxSession.as_view(), name='influx_session'),
url(r'^$',
views.InfluxMetrics.as_view(), name='influx_submit'),
]

email_urls = [
url(r'^$', views.EmailList.as_view(), name='email_list'),
url(r'^(?P<pk>[0-9]+)/$',
Expand Down Expand Up @@ -271,7 +278,12 @@
url(r'^community_surveys/', include(community_survey_urls)),
]

internal_urls = [
url(r'^events/', include(event_tracking_urls)),
]

urlpatterns = [
url(r'^$', views.ApiRootView.as_view(), name='api_root_view'),
url(r'^v1/', include(v1_urls)),
url(r'^internal/', include(internal_urls)),
]
1 change: 1 addition & 0 deletions galaxy/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@
from .users import * # noqa
from .views import * # noqa
from .survey import * # noqa
from .influx import * # noqa
99 changes: 99 additions & 0 deletions galaxy/api/views/influx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# (c) 2012-2018, Ansible by Red Hat
#
# This file is part of Ansible Galaxy
#
# Ansible Galaxy is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by
# the Apache Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Ansible Galaxy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License
# along with Galaxy. If not, see <http://www.apache.org/licenses/>.

import logging
import datetime

from galaxy.main import models
from galaxy.api import serializers

from . import base_views

from rest_framework.response import Response
from rest_framework import views

logger = logging.getLogger(__name__)

__all__ = [
'InfluxSession',
'InfluxMetrics'
]


def set_cookie(response, session):
expiration = datetime.datetime.utcnow() + datetime.timedelta(hours=1)

response.set_cookie(
'influx_session',
session,
expires=expiration
)

return response


class InfluxSession(base_views.ListCreateAPIView):
model = models.InfluxSessionIdentifier
serializer_class = serializers.InfluxSessionSerializer

def post(self, request, *args, **kwargs):
influx_session = ''
if not request.COOKIES.get('influx_session'):
influx_session = self.model.objects.create()
else:
influx_session = models.InfluxSessionIdentifier.objects.get(
session_id=request.COOKIES.get('influx_session')
)

serializer = self.get_serializer(instance=influx_session)
headers = self.get_success_headers(serializer.data)

response = set_cookie(
Response(serializer.data, headers=headers),
influx_session.session_id
)

return response

def get(self, request, *args, **kwargs):
return ''


class InfluxMetrics(views.APIView):
def get(self, request):
# TODO: Return real response
return Response('HOWDY')

def post(self, request):
serializer = self.load_serializer(request)
if serializer:
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)

# TODO: Return real response
return Response('NO GO')

# Can't name this get_serializer() for some reason
def load_serializer(self, request):
if request.data['measurement'] in serializers.InfluxTypes:
serializer_type = serializers.InfluxTypes[
request.data['measurement']
]
return serializer_type(data=request.data)
else:
return None
34 changes: 34 additions & 0 deletions galaxy/main/migrations/0118_influxsessionidentifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-24 18:24
from __future__ import unicode_literals

from django.db import migrations, models
import galaxy.main.mixins
import uuid


class Migration(migrations.Migration):

dependencies = [
('main', '0117_namespace_search_index'),
]

operations = [
migrations.CreateModel(
name='InfluxSessionIdentifier',
fields=[
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('session_id', models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False
)),
],
options={
'abstract': False,
},
bases=(models.Model, galaxy.main.mixins.DirtyMixin),
),
]
Loading