1,005 changes: 1,005 additions & 0 deletions docs/intermediate/monitoring/api/index.rst

Large diffs are not rendered by default.

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
468 changes: 468 additions & 0 deletions docs/intermediate/monitoring/geonode_monitoring/index.rst

Large diffs are not rendered by default.

430 changes: 6 additions & 424 deletions docs/intermediate/monitoring/index.rst

Large diffs are not rendered by default.

539 changes: 539 additions & 0 deletions docs/intermediate/monitoring/notifications/index.rst

Large diffs are not rendered by default.

315 changes: 315 additions & 0 deletions docs/intermediate/monitoring/user_analytics/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
Monitoring: User Analytics
==========================

Purpose
-------

UA should provide information about GeoNode resources usage at user level (not request level, like plain monitoring).

Requests
--------

1. total number of unique visitors on GeoNode (excluding ows requests) per day. This gives a base view of the reach.

* requests from all sessions of all types, ows and non-ows

``GET /monitoring/api/metric_data/request.users/?last=(x*86400)&interval=86400&group_by=label``

* non-ows related

``GET /monitoring/api/metric_data/request.users/?``
``last=(x*86400)&interval=86400&group_by=label&event_type=other``

* only ows related

``GET /monitoring/api/metric_data/request.users/?``
``last=(x*86400)&interval=86400&group_by=label&event_type=OWS:ALL``

.. code-block:: json
{
"data": {
"input_valid_from": "2018-07-11T15:41:06.419Z",
"input_valid_to": "2018-07-12T15:41:06.419Z",
"data": [
{
"valid_from": "2018-07-11T15:41:06.419Z",
"data": [
{
"samples_count": 82,
"val": 9,
"min": "0.0000",
"max": "24.0000",
"sum": "82.0000",
"metric_count": 16
}
],
"valid_to": "2018-07-12T15:41:06.419Z"
}
],
"metric": "request.users",
"interval": 86400.0,
"type": "value",
"axis_label": "Count",
"label": null
}
}
2. total number of unique visitors per URL (excluding ows requests). Let me see how many users visits the layers page or the maps page

* get number of unique tracking ids for urls

``GET /monitoring/api/metric_data/request.users/?``
``last=(x*86400)&interval=86400&group_by=resource_no_label&resource_type=url&event_type=other``

.. code-block:: json
{
"data": {
"input_valid_from": "2018-07-11T15:39:25.126Z",
"input_valid_to": "2018-07-12T15:39:25.126Z",
"data": [
{
"valid_from": "2018-07-11T15:39:25.126Z",
"data": [
{
"resource": {
"type": "url",
"name": "/layers/",
"id": 15
},
"metric_count": 2,
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "2.0000",
"samples_count": 2
},
{
"resource": {
"type": "url",
"name": "/",
"id": 16
},
"metric_count": 2,
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "2.0000",
"samples_count": 2
},
{
"resource": {
"type": "url",
"name": "/documents/",
"id": 21
},
"metric_count": 1,
"val": 1,
"min": "1.0000",
"max": "1.0000",
"sum": "1.0000",
"samples_count": 1
}
],
"valid_to": "2018-07-12T15:39:25.126Z"
}
],
"metric": "request.users",
"interval": 86400.0,
"type": "value",
"axis_label": "Count",
"label": null
}
}
3. total number of unique visitors per event_type: for example total number of unique visits of resource pages (indipendently by resource type and id)

* to get number of requests

``GET /monitoring/api/metric_data/request.users/?``
``last=86400&interval=86400&group_by=event_type``

* to get number of unique tracking ids

``GET /monitoring/api/metric_data/request.users/?``
``last=86400&interval=86400&group_by=event_type_on_label``

* to get number of unique tracking ids for specific resource type

``GET /monitoring/api/metric_data/request.users/?``
``last=86400&interval=86400&group_by=event_type_on_label&resource_type=url``

.. code-block:: json
{
"data": {
"input_valid_from": "2018-07-11T17:54:41.467Z",
"input_valid_to": "2018-07-12T17:54:41.467Z",
"data": [
{
"valid_from": "2018-07-11T17:54:41.467Z",
"data": [
{
"samples_count": 5,
"event_type": "all",
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "5.0000",
"metric_count": 5
},
{
"samples_count": 5,
"event_type": "other",
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "5.0000",
"metric_count": 5
},
{
"samples_count": 5,
"event_type": "view",
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "5.0000",
"metric_count": 5
}
],
"valid_to": "2018-07-12T17:54:41.467Z"
}
],
"metric": "request.users",
"interval": 86400.0,
"type": "value",
"axis_label": "Count",
"label": null
}
}
4. total number of unique visitors per event_type and single resource: let me see what was the most visited map page in this day, or what was the most downloaded document, what was the most requested ows layer, etc.

* list of most visited resources of `url` type

``GET /monitoring/api/metric_data/request.users/?``
``last=86400&interval=86400&group_by=resource_no_label&resource_type=url``

* list of unique tracking ids for each resource (can be narrowed down to specific resource type with `resource_type` values).

``GET /monitoring/api/metric_data/request.users/?``
``last=86400&interval=86400&group_by=resource_no_label``

.. code-block:: json
{
"data": {
"input_valid_from": "2018-07-11T17:56:49.381Z",
"input_valid_to": "2018-07-12T17:56:49.381Z",
"data": [
{
"valid_from": "2018-07-11T17:56:49.381Z",
"data": [
{
"resource": {
"type": "",
"name": "",
"id": 1
},
"metric_count": 16,
"val": 9,
"min": "0.0000",
"max": "24.0000",
"sum": "82.0000",
"samples_count": 82
},
{
"resource": {
"type": "layer",
"name": "geonode:ne_50m_admin_0_countries_lakes",
"id": 2
},
"metric_count": 4,
"val": 3,
"min": "0.0000",
"max": "2.0000",
"sum": "3.0000",
"samples_count": 3
},
{
"resource": {
"type": "layer",
"name": "geonode:world_iso2",
"id": 12
},
"metric_count": 4,
"val": 2,
"min": "0.0000",
"max": "5.0000",
"sum": "8.0000",
"samples_count": 8
},
{
"resource": {
"type": "url",
"name": "/layers/",
"id": 15
},
"metric_count": 2,
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "2.0000",
"samples_count": 2
},
{
"resource": {
"type": "url",
"name": "/",
"id": 16
},
"metric_count": 2,
"val": 2,
"min": "1.0000",
"max": "1.0000",
"sum": "2.0000",
"samples_count": 2
},
{
"resource": {
"type": "url",
"name": "/documents/",
"id": 21
},
"metric_count": 1,
"val": 1,
"min": "1.0000",
"max": "1.0000",
"sum": "1.0000",
"samples_count": 1
},
{
"resource": {
"type": "document",
"name": "GeoServer Configuration.pdf",
"id": 22
},
"metric_count": 1,
"val": 1,
"min": "5.0000",
"max": "5.0000",
"sum": "5.0000",
"samples_count": 5
}
],
"valid_to": "2018-07-12T17:56:49.381Z"
}
],
"metric": "request.users",
"interval": 86400.0,
"type": "value",
"axis_label": "Count",
"label": null
}
}
10 changes: 7 additions & 3 deletions geonode/documents/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@

from .views import DocumentUploadView, DocumentUpdateView
from . import views
from geonode.monitoring import register_url_event

js_info_dict = {
'packages': ('geonode.documents',),
}

documents_list = register_url_event()(TemplateView.as_view(
template_name='documents/document_list.html'))

urlpatterns = [ # 'geonode.documents.views',
url(r'^$',
TemplateView.as_view(
template_name='documents/document_list.html'),
documents_list,
{'facet_type': 'documents'},
name='document_browse'),
name='document_browse'
),
url(r'^(?P<docid>\d+)/?$',
views.document_detail, name='document_detail'),
url(r'^(?P<docid>\d+)/download/?$',
Expand Down
29 changes: 21 additions & 8 deletions geonode/documents/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from geonode.utils import build_social_links
from geonode.groups.models import GroupProfile
from geonode.base.views import batch_modify
from geonode.monitoring import register_event

logger = logging.getLogger("geonode.documents.views")

Expand Down Expand Up @@ -153,6 +154,8 @@ def document_detail(request, docid):
from geonode.favorite.utils import get_favorite_info
context_dict["favorite_info"] = get_favorite_info(request.user, document)

register_event(request, 'view', document)

return render(
request,
"documents/document_detail.html",
Expand All @@ -163,8 +166,8 @@ def document_download(request, docid):
document = get_object_or_404(Document, pk=docid)

if settings.MONITORING_ENABLED and document:
if hasattr(document, 'alternate'):
request.add_resource('document', document.alternate)
dtitle = getattr(document, 'alternate', None) or document.title
request.register_event('upload', 'document', dtitle)

if not request.user.has_perm(
'base.download_resourcebase',
Expand All @@ -173,6 +176,7 @@ def document_download(request, docid):
loader.render_to_string(
'401.html', context={
'error_message': _("You are not allowed to view this document.")}, request=request), status=401)
register_event(request, 'download', document)
return DownloadResponse(document.doc_file)


Expand Down Expand Up @@ -262,9 +266,16 @@ def form_valid(self, form):
bbox_y0=bbox_y0,
bbox_y1=bbox_y1)

if getattr(settings, 'MONITORING_ENABLED', False) and self.object:
if hasattr(self.object, 'alternate'):
self.request.add_resource('document', self.object.alternate)
if getattr(settings, 'SLACK_ENABLED', False):
try:
from geonode.contrib.slack.utils import build_slack_message_document, send_slack_message
send_slack_message(
build_slack_message_document(
"document_new", self.object))
except BaseException:
print "Could not send slack message for new document."

register_event(self.request, 'upload', self.object)

if self.request.GET.get('no__redirect', False):
out['success'] = True
Expand Down Expand Up @@ -307,9 +318,7 @@ def form_valid(self, form):
If the form is valid, save the associated model.
"""
self.object = form.save()
if settings.MONITORING_ENABLED and self.object:
if hasattr(self.object, 'alternate'):
self.request.add_resource('document', self.object.alternate)
register_event(self.request, 'change', self.object)
return HttpResponseRedirect(
reverse(
'document_metadata',
Expand Down Expand Up @@ -434,6 +443,7 @@ def document_metadata(
document.save()
document_form.save_many2many()

register_event(request, 'change_metadata', document)
if not ajax:
return HttpResponseRedirect(
reverse(
Expand Down Expand Up @@ -490,6 +500,7 @@ def document_metadata(
document_form.fields['is_approved'].widget.attrs.update(
{'disabled': 'true'})

register_event(request, 'view_metadata', document)
return render(request, template, context={
"resource": document,
"document": document,
Expand Down Expand Up @@ -625,6 +636,7 @@ def document_remove(request, docid, template='documents/document_remove.html'):
if request.method == 'POST':
document.delete()

register_event(request, 'remove', document)
return HttpResponseRedirect(reverse("document_browse"))
else:
return HttpResponse("Not allowed", status=403)
Expand Down Expand Up @@ -653,6 +665,7 @@ def document_metadata_detail(
except GroupProfile.DoesNotExist:
group = None
site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL
register_event(request, 'view_metadata', document)
return render(request, template, context={
"resource": document,
"group": group,
Expand Down
5 changes: 4 additions & 1 deletion geonode/layers/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
from geonode.utils import check_ogc_backend
from django.conf.urls import url
from django.views.generic import TemplateView
from geonode.monitoring import register_url_event

from . import views

js_info_dict = {
'packages': ('geonode.layers',),
}

layers_list = register_url_event()(TemplateView.as_view(template_name='layers/layer_list.html'))

urlpatterns = [
# 'geonode.layers.views',
url(r'^$',
TemplateView.as_view(template_name='layers/layer_list.html'),
layers_list,
{'facet_type': 'layers', 'is_layer': True},
name='layer_browse'),
url(r'^upload$', views.layer_upload, name='layer_upload'),
Expand Down
27 changes: 15 additions & 12 deletions geonode/layers/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
from geonode.base.views import batch_modify
from geonode.base.models import Thesaurus
from geonode.maps.models import Map
from geonode.monitoring import register_event
from geonode.geoserver.helpers import (gs_catalog,
ogc_server_settings,
set_layer_style) # cascading_delete
Expand Down Expand Up @@ -294,16 +295,9 @@ def layer_upload(request, template='upload/layer_upload.html'):
out['errormsgs'] = errormsgs
if out['success']:
status_code = 200
register_event(request, 'upload', saved_layer)
else:
status_code = 400
if settings.MONITORING_ENABLED:
layer_name = None
if saved_layer and hasattr(saved_layer, 'alternate'):
layer_name = saved_layer.alternate
elif name:
layer_name = name
if layer_name:
request.add_resource('layer', layer_name)

# null-safe charset
layer_charset = 'UTF-8'
Expand Down Expand Up @@ -759,6 +753,8 @@ def sld_definition(style):
from geonode.favorite.utils import get_favorite_info
context_dict["favorite_info"] = get_favorite_info(request.user, layer)

register_event(request, 'view', layer)

return TemplateResponse(
request, template, context=context_dict)

Expand Down Expand Up @@ -851,6 +847,7 @@ def layer_feature_catalogue(
'attributes': attributes,
'metadata': settings.PYCSW['CONFIGURATION']['metadata:main']
}
register_event(request, 'view', layer)
return render(
request,
template,
Expand All @@ -875,6 +872,7 @@ def layer_metadata(
extra=0,
form=LayerAttributeForm,
)

topic_category = layer.category

poc = layer.poc
Expand Down Expand Up @@ -936,6 +934,7 @@ def layer_metadata(
la for la in default_map_config(request)[1] if la.ows_url is None]

if request.method == "POST":

if layer.metadata_uploaded_preserve: # layer metadata cannot be edited
out = {
'success': False,
Expand Down Expand Up @@ -1120,12 +1119,14 @@ def layer_metadata(
logger.error(tb)

layer.tkeywords.add(*tkeywords_to_add)
register_event(request, 'change_metadata', layer)
except BaseException:
tb = traceback.format_exc()
logger.error(tb)

return HttpResponse(json.dumps({'message': message}))

register_event(request, 'view_metadata', layer)
if settings.ADMIN_MODERATE_UPLOADS:
if not request.user.is_superuser:
layer_form.fields['is_published'].widget.attrs.update(
Expand Down Expand Up @@ -1211,16 +1212,13 @@ def layer_metadata_advanced(request, layername):
def layer_change_poc(request, ids, template='layers/layer_change_poc.html'):
layers = Layer.objects.filter(id__in=ids.split('_'))

if settings.MONITORING_ENABLED:
for _l in layers:
if hasattr(_l, 'alternate'):
request.add_resource('layer', _l.alternate)
if request.method == 'POST':
form = PocForm(request.POST)
if form.is_valid():
for layer in layers:
layer.poc = form.cleaned_data['contact']
layer.save()

# Process the data in form.cleaned_data
# ...
# Redirect after POST
Expand Down Expand Up @@ -1316,6 +1314,7 @@ def layer_replace(request, layername, template='layers/layer_replace.html'):

if out['success']:
status_code = 200
register_event(request, 'change', layer)
else:
status_code = 400
return HttpResponse(
Expand Down Expand Up @@ -1366,6 +1365,8 @@ def layer_remove(request, layername, template='layers/layer_remove.html'):
messages.error(request, message)
return render(
request, template, context={"layer": layer})

register_event(request, 'remove', layer)
return HttpResponseRedirect(reverse("layer_browse"))
else:
return HttpResponse("Not allowed", status=403)
Expand Down Expand Up @@ -1514,6 +1515,8 @@ def layer_metadata_detail(
group = None
site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL

register_event(request, 'view_metadata', layer)

return render(request, template, context={
"resource": layer,
"perms_list": get_perms(
Expand Down
5 changes: 4 additions & 1 deletion geonode/maps/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from geonode import geoserver, qgis_server
from geonode.utils import check_ogc_backend
from geonode.monitoring import register_url_event
from . import views

js_info_dict = {
Expand Down Expand Up @@ -57,10 +58,12 @@
map_json = MapUpdateView.as_view()
map_thumbnail = set_thumbnail_map

maps_list = register_url_event()(TemplateView.as_view(template_name='maps/map_list.html'))

urlpatterns = [
# 'geonode.maps.views',
url(r'^$',
TemplateView.as_view(template_name='maps/map_list.html'),
maps_list,
{'facet_type': 'maps'},
name='maps_browse'),
url(r'^new$', new_map_view, name="new_map"),
Expand Down
41 changes: 39 additions & 2 deletions geonode/maps/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
from geonode.people.forms import ProfileForm
from geonode.base.views import batch_modify
from .tasks import delete_map
from geonode.monitoring import register_event
from geonode.monitoring.models import EventType
from requests.compat import urljoin

if check_ogc_backend(geoserver.BACKEND_PACKAGE):
Expand Down Expand Up @@ -112,7 +114,6 @@ def _resolve_map(request, id, permission='base.change_resourcebase',


# BASIC MAP VIEWS #

def map_detail(request, mapid, snapshot=None, template='maps/map_detail.html'):
'''
The view that show details of each map
Expand All @@ -135,6 +136,9 @@ def map_detail(request, mapid, snapshot=None, template='maps/map_detail.html'):
else:
config = snapshot_config(snapshot, map_obj, request)

if settings.MONITORING_ENABLED:
request.register_event('view', 'map', map_obj.title)

config = json.dumps(config)
layers = MapLayer.objects.filter(map=map_obj.id)
links = map_obj.link_set.download()
Expand Down Expand Up @@ -174,6 +178,8 @@ def map_detail(request, mapid, snapshot=None, template='maps/map_detail.html'):
from geonode.favorite.utils import get_favorite_info
context_dict["favorite_info"] = get_favorite_info(request.user, map_obj)

register_event(request, EventType.EVENT_VIEW, request.path)

return render(request, template, context=context_dict)


Expand Down Expand Up @@ -255,6 +261,7 @@ def map_metadata(
map_obj.category = new_category
map_obj.save()

register_event(request, 'change_metadata', map_obj)
if not ajax:
return HttpResponseRedirect(
reverse(
Expand Down Expand Up @@ -326,6 +333,7 @@ def map_metadata(
map_form.fields['is_approved'].widget.attrs.update(
{'disabled': 'true'})

register_event(request, 'view_metadata', map_obj)
return render(request, template, context={
"config": json.dumps(config),
"resource": map_obj,
Expand Down Expand Up @@ -365,7 +373,26 @@ def map_remove(request, mapid, template='maps/map_remove.html'):
"map": map_obj
})
elif request.method == 'POST':
delete_map.delay(object_id=map_obj.id)
if getattr(settings, 'SLACK_ENABLED', False):
slack_message = None
try:
from geonode.contrib.slack.utils import build_slack_message_map
slack_message = build_slack_message_map("map_delete", map_obj)
except BaseException:
logger.error("Could not build slack message for delete map.")

delete_map.delay(object_id=map_obj.id)

try:
from geonode.contrib.slack.utils import send_slack_messages
send_slack_messages(slack_message)
except BaseException:
logger.error("Could not send slack message for delete map.")
else:
delete_map.delay(object_id=map_obj.id)

register_event(request, 'remove', map_obj)

return HttpResponseRedirect(reverse("maps_browse"))


Expand All @@ -390,6 +417,7 @@ def map_embed(
config = snapshot_config(
snapshot, map_obj, request)

register_event(request, 'view', map_obj)
return render(request, template, context={
'config': json.dumps(config)
})
Expand Down Expand Up @@ -472,6 +500,7 @@ def map_embed_widget(request, mapid,
'map_bbox': map_bbox,
'map_layers': layers
}
register_event(request, 'view', map_obj)
message = render(request, template, context)
return HttpResponse(message)

Expand Down Expand Up @@ -519,6 +548,7 @@ def map_view(request, mapid, snapshot=None, layer_name=None,
config = add_layers_to_map_config(
request, map_obj, (layer_name, ), False)

register_event(request, 'view', request.path)
return render(request, template, context={
'config': json.dumps(config),
'map': map_obj,
Expand Down Expand Up @@ -579,6 +609,7 @@ def map_json(request, mapid, snapshot=None):
map=map_obj,
user=request.user)

register_event(request, 'change', map_obj)
return HttpResponse(
json.dumps(
map_obj.viewer_json(request)))
Expand Down Expand Up @@ -700,6 +731,7 @@ def new_map_json(request):
except ValueError as e:
return HttpResponse(str(e), status=400)
else:
register_event(request, 'upload', map_obj)
return HttpResponse(
json.dumps({'id': map_obj.id}),
status=200,
Expand Down Expand Up @@ -1088,6 +1120,9 @@ def perm_filter(layer):
[_l for _l in downloadable_layers if _l.name == lyr.name]) == 0:
downloadable_layers.append(lyr)
site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL

register_event(request, EventType.EVENT_DOWNLOAD, map_obj)

return render(request, template, context={
"geoserver": ogc_server_settings.PUBLIC_LOCATION,
"map_status": map_status,
Expand Down Expand Up @@ -1135,6 +1170,7 @@ def map_wms(request, mapid):
layerGroupName=layerGroupName,
ows=getattr(ogc_server_settings, 'ows', ''),
)
register_event(request, 'publish', map_obj)
return HttpResponse(
json.dumps(response),
content_type="application/json")
Expand Down Expand Up @@ -1357,6 +1393,7 @@ def map_metadata_detail(
except GroupProfile.DoesNotExist:
group = None
site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL
register_event(request, 'view_metadata', map_obj)
return render(request, template, context={
"resource": map_obj,
"group": group,
Expand Down
54 changes: 54 additions & 0 deletions geonode/monitoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

import logging
from django.utils.translation import ugettext_noop as _
from django.conf import settings
from functools import wraps
import types

from geonode.notifications_helper import NotificationsAppConfigBase, has_notifications
from django.db.models.signals import post_migrate
Expand Down Expand Up @@ -50,3 +53,54 @@ def ready(self):


default_app_config = 'geonode.monitoring.MonitoringAppConfig'


def register_url_event(event_type=None):
"""
Decorator on views, which will register url event
usage:
>> register_url_event()(TemplateView.view_as_view())
"""
def _register_url_event(view):
@wraps(view)
def inner(*args, **kwargs):
if settings.MONITORING_ENABLED:
request = args[0]
register_event(request, event_type or 'view', request.path)
return view(*args, **kwargs)
return inner
return _register_url_event


def register_event(request, event_type, resource):
"""
Wrapper function to be used inside views to collect event and resource
@param request Request object
@param event_type name of event type
@param resource string (then resource type will be url) or Resource instance
>>> from geonode.monitoring import register_event
>>> def view(request):
register_event(request, 'view', layer)
"""
if not settings.MONITORING_ENABLED:
return
from geonode.base.models import ResourceBase
if isinstance(resource, types.StringTypes):
resource_type = 'url'
resource_name = request.path
elif isinstance(resource, ResourceBase):
resource_type = resource.__class__._meta.verbose_name_raw
resource_name = getattr(resource, 'alternate', None) or resource.title
else:
raise ValueError("Invalid resource: {}".format(resource))
request.register_event(event_type, resource_type, resource_name)


def register_proxy_event(request):
"""
Process request to geoserver proxy. Extract layer and ows type
"""
10 changes: 5 additions & 5 deletions geonode/monitoring/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@
MetricNotificationCheck,
NotificationMetricDefinition,
NotificationReceiver,
OWSService,)
EventType,)


@admin.register(Host)
class HostAdmin(admin.ModelAdmin):
list_display = ('name', 'active',)


@admin.register(OWSService)
class OWSServiceAdmin(admin.ModelAdmin):
@admin.register(EventType)
class EventTypeAdmin(admin.ModelAdmin):
list_display = ('name', )


Expand Down Expand Up @@ -63,8 +63,8 @@ class MetricAdmin(admin.ModelAdmin):
@admin.register(RequestEvent)
class RequestEvent(admin.ModelAdmin):
list_display = ('service', 'created', 'received', 'request_method', 'response_status',
'ows_service', 'response_size', 'client_country', 'request_path')
list_filter = ('host', 'service', 'request_method', 'response_status', 'ows_service',)
'event_type', 'response_size', 'client_country', 'request_path')
list_filter = ('host', 'service', 'request_method', 'response_status', 'event_type',)


@admin.register(MetricLabel)
Expand Down
35 changes: 35 additions & 0 deletions geonode/monitoring/aggregate_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
#########################################################################
#
# Copyright (C) 2019 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

from __future__ import print_function
import logging

from django.core.management.base import BaseCommand

from geonode.monitoring.collector import CollectorAPI

log = logging.getLogger(__name__)


class Command(BaseCommand):

def handle(self, *args, **kwargs):
c = CollectorAPI()
c.aggregate_past_periods()
302 changes: 302 additions & 0 deletions geonode/monitoring/aggregation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# -*- coding: utf-8 -*-
#########################################################################
#
# Copyright (C) 2018 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

from datetime import datetime, timedelta, time
from decimal import Decimal
import logging

import pytz

from django.conf import settings
from django.db.models import Sum, F
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned

from geonode.monitoring.utils import generate_periods
from geonode.monitoring.models import (Metric, MetricValue, ServiceTypeMetric,
MonitoredResource, MetricLabel, EventType,)


log = logging.getLogger(__name__)


def get_metric_names():

"""
Returns list of tuples: (service type, list of metrics)
"""
q = ServiceTypeMetric.objects.all().select_related(
).order_by('service_type', 'metric')

out = []
current_service = None
current_set = []
for item in q:
service, metric = item.service_type, item.metric
if current_service != service:
if current_service is not None:
out.append((current_service, current_set,))
current_set = []
current_service = service
current_set.append(metric)
if current_set:
out.append((current_service, current_set,))

return out


def get_labels_for_metric(metric_name, resource=None):
mt = ServiceTypeMetric.objects.filter(metric__name=metric_name)
if not mt:
raise ValueError("No metric for {}".format(metric_name))

qparams = {'metric_values__service_metric__in': mt}
if resource:
qparams['metricvalue__resource'] = resource
return list(MetricLabel.objects.filter(
**qparams).distinct().values_list('id', 'name'))


def get_resources_for_metric(metric_name):
mt = ServiceTypeMetric.objects.filter(metric__name=metric_name)
if not mt:
raise ValueError("No metric for {}".format(metric_name))
return list(MonitoredResource.objects.filter(metric_values__service_metric__in=mt)
.exclude(name='', type='')
.distinct()
.order_by('type', 'name')
.values_list('type', 'name'))


def extract_resources(requests):
resources = MonitoredResource.objects.filter(
requests__in=requests).distinct()
out = []
for res in resources:
out.append((res, requests.filter(resources=res).distinct(),))
return out


def extract_event_type(requests):
q = requests.exclude(event_type__isnull=True).distinct(
'event_type').values_list('event_type', flat=True)
try:
return q.get()
except (ObjectDoesNotExist, MultipleObjectsReturned,):
pass


def extract_event_types(requests):
event_types = requests.exclude(event_type__isnull=True)\
.distinct('event_type')\
.values_list('event_type', flat=True)
return [EventType.objects.get(id=evt_id) for evt_id in event_types]


def extract_special_event_types(requests):
"""
Return list of pairs (event_type, requests)
that should be registered as one of aggregating event types: ows:all, other,
"""
out = []

ows_et = requests.exclude(event_type__isnull=True)\
.filter(event_type__name__startswith='OWS:')\
.exclude(event_type__name=EventType.EVENT_OWS)\
.distinct('event_type')\
.values_list('event_type', flat=True)
ows_rq = requests.filter(event_type__in=ows_et)
ows_all = EventType.get(EventType.EVENT_OWS)
out.append((ows_all, ows_rq,))

nonows_et = requests.exclude(event_type__isnull=True)\
.exclude(event_type__name__startswith='OWS:')\
.exclude(event_type__name=EventType.EVENT_OTHER)\
.distinct('event_type')\
.values_list('event_type', flat=True)
nonows_rq = requests.filter(event_type__in=nonows_et)
nonows_all = EventType.get(EventType.EVENT_OTHER)
out.append((nonows_all, nonows_rq,))

return out


def calculate_rate(metric_name, metric_label,
current_value, valid_to):
"""
Find previous network metric value and caclulate rate between them
"""
prev = MetricValue.objects.filter(service_metric__metric__name=metric_name,
label__name=metric_label,
valid_to__lt=valid_to)\
.order_by('-valid_to').first()
if not prev:
return
prev_val = prev.value_num
valid_to = valid_to.replace(tzinfo=pytz.utc)
prev.valid_to = prev.valid_to.replace(tzinfo=pytz.utc)
interval = valid_to - prev.valid_to
if not isinstance(current_value, Decimal):
current_value = Decimal(current_value)

# this means counter was reset, don't want rates below 0
if current_value < prev_val:
return
rate = float((current_value - prev_val)) / interval.total_seconds()
return rate


def calculate_percent(
metric_name, metric_label, current_value, valid_to):
"""
Find previous network metric value and caclulate percent
"""
rate = calculate_rate(
metric_name, metric_label, current_value, valid_to)
if rate is None:
return
return rate * 100


def adjust_now_to_noon(now):
return pytz.utc.localize(datetime.combine(now.date(), time(0, 0, 0)))


def aggregate_past_periods(metric_data_q=None, periods=None, cleanup=True, now=None, max_since=None):
"""
Aggregate past metric data into longer periods
@param metric_data_q Query for metric data to use as input
(default: all MetricValues)
@param periods list of tuples (cutoff, aggregation) to be used
(default: settings.MONITORING_DATA_AGGREGATION)
@param cleanup flag if input data should be removed after aggregation
(default: True)
@param now arbitrary now moment to start calculation of cutoff
(default: current now)
@param max_since look for data no older than max_since
(default: 1 year)
"""
utc = pytz.utc
if now is None:
now = datetime.utcnow().replace(tzinfo=utc)
if metric_data_q is None:
metric_data_q = MetricValue.objects.all()
if periods is None:
periods = settings.MONITORING_DATA_AGGREGATION
max_since = max_since or now - timedelta(days=356)
previous_cutoff = None
counter = 0
now = adjust_now_to_noon(now)
# start from the end, oldest one first
for cutoff_base, aggregation_period in reversed(periods):
since = previous_cutoff or max_since
until = now - cutoff_base

if since > until:
log.info("Wrong period boundaries, end %s is before start %s, agg: %s",
until, since, aggregation_period)
previous_cutoff = max(until, since)
continue

log.info("aggregation params: cutoff: %s agg period: %s"
"\n since: '%s' until '%s', but previous cutoff:"
" '%s', aggregate to '%s'",
cutoff_base, aggregation_period, since, until, previous_cutoff, aggregation_period)

periods = generate_periods(since, aggregation_period, end=until)

# for each target period we select mertic values within it
# and extract service, resource, event type and label combinations
# then, for each distinctive set, calculate per-metric aggregate values
for period_start, period_end in periods:
log.info('period %s - %s (%s s)', period_start, period_end, period_end-period_start)
ret = aggregate_period(period_start, period_end, metric_data_q, cleanup)
counter += ret
previous_cutoff = until
return counter


def aggregate_period(period_start, period_end, metric_data_q, cleanup=True):
counter = 0
to_remove_data = {'remove_at': period_start.strftime("%Y%m%d%H%M%S")}
source_metric_data = metric_data_q.filter(valid_from__gte=period_start,
valid_to__lte=period_end)\
.exclude(valid_from=period_start,
valid_to=period_end,
data={})
r = source_metric_data.values_list('service_id', 'service_metric_id', 'resource_id', 'event_type_id', 'label_id',)\
.distinct('service_id', 'service_metric_id', 'resource_id', 'event_type_id', 'label_id')
source_metric_data.update(data=to_remove_data)

for service_id, metric_id, resource_id, event_type_id, label_id in r:
m = Metric.objects.filter(service_type__id=metric_id).get()
f = m.get_aggregate_field()
per_metric_q = source_metric_data.filter(service_metric_id=metric_id,
service_id=service_id,
resource_id=resource_id,
event_type_id=event_type_id,
label_id=label_id)

try:
value_q = per_metric_q.aggregate(fvalue=f,
fsamples_count=Sum(F('samples_count')))
except TypeError, err:
raise ValueError(f, m, err)
value = value_q['fvalue']
samples_count = value_q['fsamples_count']
if cleanup:
per_metric_q.delete()
log.info('Metric %s: %s - %s (value: %s, samples: %s)',
m, period_start, period_end, value, samples_count)
if not metric_data_q.filter(service_metric_id=metric_id,
service_id=service_id,
resource_id=resource_id,
event_type_id=event_type_id,
valid_from=period_start,
valid_to=period_end,
label_id=label_id).exists():
MetricValue.objects.create(service_metric_id=metric_id,
service_id=service_id,
resource_id=resource_id,
event_type_id=event_type_id,
value=value,
value_num=value,
value_raw=value,
valid_from=period_start,
valid_to=period_end,
label_id=label_id,
samples_count=samples_count)
else:
metric_data_q.filter(service_metric_id=metric_id,
service_id=service_id,
resource_id=resource_id,
event_type_id=event_type_id,
valid_from=period_start,
valid_to=period_end,
label_id=label_id)\
.update(value=value,
value_num=value,
value_raw=value,
data=None,
samples_count=samples_count)
counter += 1

if cleanup:
source_metric_data.filter(data=to_remove_data).delete()
return counter
525 changes: 204 additions & 321 deletions geonode/monitoring/collector.py

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions geonode/monitoring/fixtures/notifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"min_value" : "0.0000",
"description" : "Number of handled requests is lower than",
"max_value" : "10.0000",
"use_ows_service" : false,
"use_event_type" : false,
"notification_check" : 2,
"metric" : 2,
"use_service" : false,
Expand All @@ -55,7 +55,7 @@
"model" : "monitoring.notificationmetricdefinition",
"fields" : {
"max_value" : null,
"use_ows_service" : false,
"use_event_type" : false,
"use_service" : false,
"use_resource" : false,
"notification_check" : 2,
Expand All @@ -76,7 +76,7 @@
"use_service" : false,
"use_label" : false,
"max_value" : null,
"use_ows_service" : false,
"use_event_type" : false,
"min_value" : "500.0000",
"description" : "Response time is higher than",
"steps" : null,
Expand All @@ -88,7 +88,7 @@
{
"pk" : 20,
"fields" : {
"ows_service" : null,
"event_type" : null,
"active" : true,
"notification_check" : 2,
"metric" : 2,
Expand All @@ -109,7 +109,7 @@
"resource" : null,
"definition" : 3,
"active" : true,
"ows_service" : null,
"event_type" : null,
"metric" : 2,
"notification_check" : 2,
"label" : null,
Expand All @@ -131,7 +131,7 @@
"max_value" : null,
"service" : null,
"active" : true,
"ows_service" : null,
"event_type" : null,
"metric" : 11,
"notification_check" : 2
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const fail = createAction(
const get = (service, interval) =>
(dispatch) => {
dispatch(begin());
let url = `${apiUrl}/metric_data/response.time/?ows_service=${service}`;
let url = `${apiUrl}/metric_data/response.time/?event_type=${service}`;
url += `&last=${interval}&interval=${interval}`;
fetch({ url })
.then(response => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const fail = createAction(
const get = () =>
(dispatch) => {
dispatch(begin());
const url = `${apiUrl}/ows_services/`;
const url = `${apiUrl}/event_types/`;
fetch({ url })
.then(response => {
dispatch(success(response));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ class WSServiceSelect extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.selected) {return;}
const services = nextProps.services;
if (services && services.ows_services && services.ows_services.length > 0) {
this.props.setService(services.ows_services[0].name);
if (services && services.event_types && services.event_types.length > 0) {
this.props.setService(services.event_types[0].name);
}
}

render() {
const items = this.props.services
? this.props.services.ows_services.map(service => (
? this.props.services.event_types.map(service => (
<MenuItem
key={service.name}
value={service.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const get = (service, argInterval) =>
(dispatch) => {
dispatch(begin());
const interval = sequenceInterval(argInterval);
let url = `${apiUrl}/metric_data/response.error.count/?ows_service=${service}`;
let url = `${apiUrl}/metric_data/response.error.count/?event_type=${service}`;
url += `&last=${argInterval}&interval=${interval}`;
fetch({ url })
.then(response => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const get = (service, argInterval) =>
(dispatch) => {
dispatch(begin());
const interval = sequenceInterval(argInterval);
let url = `${apiUrl}/metric_data/response.time/?ows_service=${service}`;
let url = `${apiUrl}/metric_data/response.time/?event_type=${service}`;
url += `&last=${argInterval}&interval=${interval}`;
fetch({ url })
.then(response => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const get = (service, argInterval) =>
(dispatch) => {
dispatch(begin());
const interval = sequenceInterval(argInterval);
let url = `${apiUrl}/metric_data/request.count/?ows_service=${service}`;
let url = `${apiUrl}/metric_data/request.count/?event_type=${service}`;
url += `&last=${argInterval}&interval=${interval}`;
fetch({ url })
.then(throughput => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ const fail = createAction(
);


const get = (interval, layer, owsService) =>
const get = (interval, layer, eventType) =>
(dispatch) => {
dispatch(begin());
const url = `${apiUrl}/metric_data/response.error.count/?last=${interval}&interval=${interval}`;
fetch({ url: `${url}&resource=${layer}&ows_service=${owsService}` })
fetch({ url: `${url}&resource=${layer}&event_type=${eventType}` })
.then(response => {
dispatch(success(response));
return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ const fail = createAction(
);


const get = (interval, layer, owsService) =>
const get = (interval, layer, eventType) =>
(dispatch) => {
dispatch(begin());
const url = `${apiUrl}/metric_data/response.time/?last=${interval}&interval=${interval}`;
fetch({ url: `${url}&resource=${layer}&ows_service=${owsService}` })
fetch({ url: `${url}&resource=${layer}&event_type=${eventType}` })
.then(response => {
dispatch(success(response));
return response;
Expand Down
35 changes: 35 additions & 0 deletions geonode/monitoring/management/commands/aggregate_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
#########################################################################
#
# Copyright (C) 2018 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################

from __future__ import print_function
import logging

from django.core.management.base import BaseCommand

from geonode.monitoring.collector import CollectorAPI

log = logging.getLogger(__name__)


class Command(BaseCommand):

def handle(self, *args, **kwargs):
c = CollectorAPI()
c.aggregate_past_periods()
51 changes: 39 additions & 12 deletions geonode/monitoring/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@

import logging
import pytz
import hashlib
import types

from datetime import datetime
from django.conf import settings
from geonode.monitoring.models import Service, Host
from geonode.monitoring.utils import MonitoringHandler, MonitoringFilter
from geonode.monitoring.utils import MonitoringHandler
from django.http import HttpResponse


Expand Down Expand Up @@ -52,8 +54,6 @@ def setup_logging(self):
self.service = self.get_service()
self.handler = MonitoringHandler(self.service)
self.handler.setLevel(logging.DEBUG)
self.filter = MonitoringFilter(self.service, FILTER_URLS)
self.handler.addFilter(self.filter)
self.log.addHandler(self.handler)

def get_service(self):
Expand All @@ -71,14 +71,25 @@ def get_service(self):
service = None

@staticmethod
def add_resource(request, resource_type, name):
def should_process(request):
current = request.path

for skip_url in settings.MONITORING_SKIP_PATHS:
if isinstance(skip_url, types.StringTypes):
if current.startswith(skip_url):
return False
elif hasattr(skip_url, 'match'):
if skip_url.match(current):
return False
return True

@staticmethod
def register_event(request, event_type, resource_type, resource_name):
m = getattr(request, '_monitoring', None)
if not m:
return
res = m['resources']
res_list = res.get(resource_type) or []
res_list.append(name)
res[resource_type] = res_list
events = m['events']
events.append((event_type, resource_type, resource_name,))

def register_request(self, request, response):
if self.service:
Expand All @@ -96,17 +107,33 @@ def process_view(self, request, view_func, view_args, view_kwargs):
del request._monitoring

def process_request(self, request):
if not self.should_process(request):
return
utc = pytz.utc
now = datetime.utcnow().replace(tzinfo=utc)

# enforce session create
if not request.session.session_key:
request.session.create()

meta = {'started': now,
'resources': {},
'finished': None}
'events': [],
'finished': None,
}

if settings.USER_ANALYTICS_ENABLED:
meta.update({
'user_identifier': hashlib.sha256(request.session.session_key or '').hexdigest(),
'user_username': request.user.username if request.user.is_authenticated() else None
})

request._monitoring = meta

def add_resource(resource_type, name):
return self.add_resource(request, resource_type, name)
def register_event(event_type, resource_type, name):
self.register_event(request, event_type, resource_type, name)

request.add_resource = add_resource
request.register_event = register_event

def process_response(self, request, response):
m = getattr(request, '_monitoring', None)
Expand Down
87 changes: 87 additions & 0 deletions geonode/monitoring/migrations/0025_auto_20190813_0808.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-08-13 08:08
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('monitoring', '0024_auto_20190605_1619'),
]

operations = [
migrations.CreateModel(
name='EventType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(choices=[(b'OWS:TMS', b'TMS'), (b'OWS:WMS-C', b'WMS-C'), (b'OWS:WMTS', b'WMTS'), (b'OWS:WCS', b'WCS'), (b'OWS:WFS', b'WFS'), (b'OWS:WMS', b'WMS'), (b'OWS:WPS', b'WPS'), (b'other', b'Non-OWS'), (b'OWS:ALL', b'Any OWS'), (b'all', b'All'), (b'create', b'Create'), (b'upload', b'Upload'), (b'change', b'Change'), (b'change_metadata', b'Change Metadata'), (b'view_metadata', b'View Metadata'), (b'view', b'View'), (b'download', b'Download'), (b'publish', b'Publish'), (b'remove', b'Remove'), (b'geoserver', b'Geoserver event')], max_length=16, unique=True)),
],
),
migrations.RemoveField(
model_name='metricnotificationcheck',
name='ows_service',
),
migrations.RemoveField(
model_name='notificationmetricdefinition',
name='use_ows_service',
),
migrations.RemoveField(
model_name='requestevent',
name='ows_service',
),
migrations.AddField(
model_name='notificationmetricdefinition',
name='use_event_type',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='requestevent',
name='user_anonymous',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='requestevent',
name='user_identifier',
field=models.CharField(blank=True, db_index=True, default=None, max_length=255, null=True),
),
migrations.AlterField(
model_name='monitoredresource',
name='type',
field=models.CharField(choices=[(b'', b'No resource'), (b'layer', b'Layer'), (b'map', b'Map'), (b'resource base', b'Resource base'), (b'document', b'Document'), (b'style', b'Style'), (b'admin', b'Admin'), (b'url', b'URL'), (b'other', b'Other')], default=b'', max_length=255),
),
migrations.AlterField(
model_name='requestevent',
name='client_ip',
field=models.GenericIPAddressField(blank=True, null=True),
),
migrations.AddField(
model_name='metricnotificationcheck',
name='event_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='monitoring.EventType'),
),
migrations.AddField(
model_name='metricvalue',
name='event_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='metric_values', to='monitoring.EventType'),
),
migrations.AddField(
model_name='requestevent',
name='event_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='monitoring.EventType'),
),
migrations.AlterUniqueTogether(
name='metricvalue',
unique_together=set(
[('valid_from', 'valid_to', 'service', 'service_metric', 'resource', 'label', 'event_type')]),
),
migrations.RemoveField(
model_name='metricvalue',
name='ows_service',
),
migrations.DeleteModel(
name='OWSService',
),
]
35 changes: 35 additions & 0 deletions geonode/monitoring/migrations/0026_auto_20190821_0736.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-08-21 07:36
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('monitoring', '0025_auto_20190813_0808'),
]

operations = [
migrations.RemoveField(
model_name='requestevent',
name='user_anonymous',
),
migrations.AddField(
model_name='requestevent',
name='user_username',
field=models.CharField(blank=True, default=None, max_length=150, null=True),
),
migrations.AlterField(
model_name='metricvalue',
name='resource',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='metric_values', to='monitoring.MonitoredResource'),
),
migrations.AlterField(
model_name='monitoredresource',
name='type',
field=models.CharField(choices=[(b'', b'No resource'), (b'layer', b'Layer'), (b'map', b'Map'), (b'resource_base', b'Resource base'), (b'document', b'Document'), (b'style', b'Style'), (b'admin', b'Admin'), (b'url', b'URL'), (b'other', b'Other')], default=b'', max_length=255),
),
]
422 changes: 310 additions & 112 deletions geonode/monitoring/models.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion geonode/monitoring/static/monitoring/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion geonode/monitoring/static/monitoring/bundle.js.map

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions geonode/monitoring/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#########################################################################
#
# Copyright (C) 2016 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
555 changes: 426 additions & 129 deletions geonode/monitoring/tests.py → geonode/monitoring/tests/integration.py

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion geonode/monitoring/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
url(r'^api/hosts/$', views.api_hosts, name='api_hosts'),
url(r'^api/labels/$', views.api_labels, name='api_labels'),
url(r'^api/resources/$', views.api_resources, name='api_resources'),
url(r'^api/ows_services/$', views.api_ows_services, name='api_ows_services'),
url(r'^api/resource_types/$', views.api_resource_types, name='api_resource_types'),
url(r'^api/event_types/$', views.api_event_types, name='api_event_types'),
url(r'^api/exceptions/$', views.api_exceptions, name='api_exceptions'),
url(r'^api/exceptions/(?P<exception_id>[\d\+]+)/$', views.api_exception, name='api_exception'),
url(r'^api/metric_data/(?P<metric_name>[\w\.]+)/$',
Expand Down
36 changes: 8 additions & 28 deletions geonode/monitoring/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
#########################################################################

import os
import re
import pytz
import types
import Queue
import logging
import xmljson
Expand All @@ -48,25 +46,6 @@
log = logging.getLogger(__name__)


class MonitoringFilter(logging.Filter):

def __init__(self, service, skip_urls=tuple(), *args, **kwargs):
super(MonitoringFilter, self).__init__(*args, **kwargs)
self.service = service
self.skip_urls = skip_urls

def filter(self, record):
fp = record.request.get_full_path()
for skip_url in self.skip_urls:
if isinstance(skip_url, types.StringTypes):
if fp.startswith(skip_url):
return False
elif isinstance(skip_url, re.RegexObject):
if skip_url.match(fp):
return False
return record


class MonitoringHandler(logging.Handler):

def __init__(self, service, *args, **kwargs):
Expand Down Expand Up @@ -238,15 +217,16 @@ def align_period_start(start, interval):
def generate_periods(since, interval, end=None, align=True):
"""
Generator of periods: tuple of [start, end).
since parameter will be aligned to closest interval before since.1
since parameter will be aligned to closest interval before since.
"""
utc = pytz.utc
end = end or datetime.utcnow().replace(tzinfo=utc)
if align:
since_aligned = align_period_start(since, interval)
else:
since_aligned = since

if end < since:
raise ValueError("End cannot be earlienr than beginning")
full_interval = (end - since).total_seconds()
_periods = divmod(full_interval, interval.total_seconds())
periods_count = _periods[0]
Expand Down Expand Up @@ -338,12 +318,12 @@ def label_type(val):
raise ValueError("Invalid label value: {}".format(val))

@staticmethod
def ows_service_type(val):
from geonode.monitoring.models import OWSService
def event_type_type(val):
from geonode.monitoring.models import EventType
try:
return OWSService.objects.get(name=val)
except OWSService.DoesNotExist:
raise ValueError("OWS Service {} doesn't exist".format(val))
return EventType.objects.get(name=val)
except EventType.DoesNotExist:
raise ValueError("Event Type {} doesn't exist".format(val))


def dump(obj, additional_fields=tuple()):
Expand Down
69 changes: 52 additions & 17 deletions geonode/monitoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@

import json
import pytz

from datetime import datetime, timedelta
from django.shortcuts import render
from django import forms
from django.contrib import auth
from django.conf import settings
from django.views.generic.base import View
from django.core.urlresolvers import reverse
Expand All @@ -39,7 +41,7 @@
MetricLabel,
MonitoredResource,
ExceptionEvent,
OWSService,
EventType,
NotificationCheck,
MetricNotificationCheck,
)
Expand Down Expand Up @@ -148,12 +150,22 @@ def _check_type(self, tname):
class MetricsFilters(CheckTypeForm):
GROUP_BY_RESOURCE = 'resource'
GROUP_BY_CHOICES = ((GROUP_BY_RESOURCE, "By resource",),)
GROUP_BY_RESOURCE_NO_LABEL = 'resource_no_label'
GROUP_BY_LABEL = 'label'
GROUP_BY_EVENT_TYPE = 'event_type'
GROUP_BY_EVENT_TYPE_ON_LABEL = 'event_type_on_label'
GROUP_BY_CHOICES = ((GROUP_BY_RESOURCE, "By resource",),
(GROUP_BY_RESOURCE_NO_LABEL, "By resource but no label",),
(GROUP_BY_LABEL, "By label",),
(GROUP_BY_EVENT_TYPE, "By event type",),
(GROUP_BY_EVENT_TYPE_ON_LABEL, "By event type on label",),
)
service = forms.CharField(required=False)
label = forms.CharField(required=False)
resource = forms.CharField(required=False)
resource_type = forms.ChoiceField(
choices=MonitoredResource.TYPES, required=False)
ows_service = forms.CharField(required=False)
event_type = forms.CharField(required=False)
service_type = forms.CharField(required=False)
group_by = forms.ChoiceField(choices=GROUP_BY_CHOICES, required=False)

Expand All @@ -166,8 +178,8 @@ def clean_service(self):
def clean_label(self):
return self._check_type('label')

def clean_ows_service(self):
return self._check_type('ows_service')
def clean_event_type(self):
return self._check_type('event_type')

def clean_service_type(self):
return self._check_type('service_type')
Expand Down Expand Up @@ -277,6 +289,28 @@ def get_queryset(self, metric_name=None,
return q


class ResourceTypesList(FilteredView):

output_name = 'resource_types'

def get(self, request, *args, **kwargs):
if self.filter_form:
f = self.filter_form(data=request.GET)
if not f.is_valid():
return json_response({'success': False,
'status': 'errors',
'errors': f.errors},
status=400)
out = [{"name": mrt[0], "type": mrt[1]} for mrt in MonitoredResource.TYPES]
data = {self.output_name: out,
'success': True,
'errors': {},
'status': 'ok'}
if self.output_name != 'data':
data['data'] = {'key': self.output_name}
return json_response(data)


class LabelsList(FilteredView):

filter_form = LabelsFilterForm
Expand Down Expand Up @@ -307,13 +341,13 @@ def get_queryset(self, metric_name, valid_from,
return q


class OWSServiceList(FilteredView):
class EventTypeList(FilteredView):

fields_map = (('name', 'name',),)
output_name = 'ows_services'
output_name = 'event_types'

def get_queryset(self, **kwargs):
return OWSService.objects.all()
return EventType.objects.all()


class MetricDataView(View):
Expand Down Expand Up @@ -436,7 +470,7 @@ def get(self, request, *args, **kwargs):


def index(request):
if request.user.is_superuser:
if auth.get_user(request).is_superuser:
return render(request, 'monitoring/index.html')
return render(request, 'monitoring/non_superuser.html')

Expand All @@ -453,7 +487,7 @@ class MetricNotificationCheckForm(forms.ModelForm):
service = forms.CharField(required=False)
resource = forms.CharField(required=False)
label = forms.CharField(required=False)
ows_service = forms.CharField(required=False)
event_type = forms.CharField(required=False)

class Meta:
model = MetricNotificationCheck
Expand Down Expand Up @@ -483,8 +517,8 @@ def clean_service(self):
def clean_label(self):
return self._get_clean_model(MetricLabel, 'label')

def clean_ows_service(self):
return self._get_clean_model(OWSService, 'ows_service')
def clean_event_type(self):
return self._get_clean_model(EventType, 'event_type')

def clean_resource(self):
val = self.cleaned_data.get('resource')
Expand Down Expand Up @@ -516,7 +550,7 @@ def get(self, request, *args, **kwargs):
'steps_calculated',
'unit',
'is_enabled',)
if request.user.is_authenticated():
if auth.get_user(request).is_authenticated():
obj = self.get_object()
out['success'] = True
out['status'] = 'ok'
Expand All @@ -535,7 +569,7 @@ def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
out = {'success': False, 'status': 'error', 'data': [], 'errors': {}}
status = 500
if request.user.is_authenticated():
if auth.get_user(request).is_authenticated():
obj = self.get_object()
try:
is_json = True
Expand Down Expand Up @@ -576,7 +610,7 @@ class NotificationsList(FilteredView):

def get_filter_args(self, *args, **kwargs):
self.errors = {}
if not self.request.user.is_authenticated():
if not auth.get_user(self.request).is_authenticated():
self.errors = {'user': ['User is not authenticated']}
return {}

Expand Down Expand Up @@ -642,13 +676,13 @@ def get(self, request, *args, **kwargs):

class AutoconfigureView(View):
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated():
if not auth.get_user(request).is_authenticated():
out = {'success': False,
'status': 'error',
'errors': {'user': ['User is not authenticated']}
}
return json_response(out, status=401)
if not (request.user.is_superuser or request.user.is_staff):
if not (auth.get_user(request).is_superuser or auth.get_user(request).is_staff):
out = {'success': False,
'status': 'error',
'errors': {'user': ['User is not permitted']}
Expand Down Expand Up @@ -691,7 +725,8 @@ def get(self, request, *args, **kwargs):
api_hosts = HostsList.as_view()
api_labels = LabelsList.as_view()
api_resources = ResourcesList.as_view()
api_ows_services = OWSServiceList.as_view()
api_resource_types = ResourceTypesList.as_view()
api_event_types = EventTypeList.as_view()
api_metric_data = MetricDataView.as_view()
api_metric_collect = CollectMetricsView.as_view()
api_exceptions = ExceptionsListView.as_view()
Expand Down
2 changes: 2 additions & 0 deletions geonode/proxy/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from geonode.base.enumerations import LINK_TYPES as _LT

from geonode import geoserver, qgis_server # noqa
from geonode.monitoring import register_event

TIMEOUT = 300

Expand Down Expand Up @@ -324,6 +325,7 @@ def download(request, resourceid, sender=Layer):
target_file_name = "".join([instance.name, ".zip"])
target_file = os.path.join(dirpath, target_file_name)
zip_dir(target_folder, target_file)
register_event(request, 'download', instance)
response = HttpResponse(
content=open(target_file),
status=200,
Expand Down
29 changes: 26 additions & 3 deletions geonode/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1159,7 +1159,7 @@
MONITORING_SERVICE_NAME = os.getenv("MONITORING_SERVICE_NAME", 'local-geonode')

# how long monitoring data should be stored
MONITORING_DATA_TTL = timedelta(days=int(os.getenv("MONITORING_DATA_TTL", 7)))
MONITORING_DATA_TTL = timedelta(days=int(os.getenv("MONITORING_DATA_TTL", 365)))

# this will disable csrf check for notification config views,
# use with caution - for dev purpose only
Expand Down Expand Up @@ -1191,8 +1191,6 @@

GEONODE_CATALOGUE_METADATA_XSL = ast.literal_eval(os.getenv('GEONODE_CATALOGUE_METADATA_XSL', 'True'))



# -- START Client Hooksets Setup

# GeoNode javascript client configuration
Expand Down Expand Up @@ -1733,6 +1731,31 @@ def get_geonode_catalogue_service():

GEOIP_PATH = os.getenv('GEOIP_PATH', os.path.join(PROJECT_ROOT, 'GeoIPCities.dat'))

# skip certain paths to not to mud stats too much
MONITORING_SKIP_PATHS = ('/api/o/',
'/monitoring/',
'/admin',
'/lang.js',
'/jsi18n',
STATIC_URL,
MEDIA_URL,
re.compile('^/[a-z]{2}/admin/'),
)

# configure aggregation of past data to control data resolution
# list of data age, aggregation, in reverse order
# for current data, 1 minute resolution
# for data older than 1 day, 1-hour resolution
# for data older than 2 weeks, 1 day resolution
MONITORING_DATA_AGGREGATION = (
(timedelta(seconds=0), timedelta(minutes=1),),
(timedelta(days=1), timedelta(minutes=60),),
(timedelta(days=14), timedelta(days=1),),
)

# privacy settings
USER_ANALYTICS_ENABLED = ast.literal_eval(os.getenv('USER_ANALYTICS_ENABLED', 'False'))

# If this option is enabled, Resources belonging to a Group won't be
# visible by others
GROUP_PRIVATE_RESOURCES = ast.literal_eval(os.environ.get('GROUP_PRIVATE_RESOURCES', 'False'))
Expand Down
148 changes: 145 additions & 3 deletions geonode/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,163 @@

from geonode.tests.base import GeoNodeBaseTestSupport

import contextlib
import os
import copy
import json
import urllib
import urllib2
import logging
import contextlib

from bs4 import BeautifulSoup
from poster.streaminghttp import register_openers
from poster.encode import multipart_encode, MultipartParam

from django.core.management import call_command
from django.db.models import signals
from django.core import mail
from django.conf import settings
from django.db.models import signals
from django.core.urlresolvers import reverse
from django.core.management import call_command

from geonode.maps.models import Layer
from geonode.geoserver.helpers import set_attributes
from geonode.geoserver.signals import geoserver_post_save
from geonode.notifications_helper import has_notifications, notifications

logger = logging.getLogger(__name__)


def upload_step(step=None):
step = reverse('data_upload', args=[step] if step else [])
return step


class Client(object):

"""client for making http requests"""

def __init__(self, url, user, passwd):
self.url = url
self.user = user
self.passwd = passwd
self.csrf_token = None
self.opener = self._init_url_opener()

def _init_url_opener(self):
self.cookies = urllib2.HTTPCookieProcessor()
opener = register_openers()
opener.add_handler(self.cookies) # Add cookie handler
return opener

def make_request(self, path, data=None,
ajax=False, debug=True):
url = path if path.startswith("http") else self.url + path
if ajax:
url += '&ajax=true' if '?' in url else '?ajax=true'
request = None
if data:
items = []
# wrap post parameters
for name, value in data.items():
if isinstance(value, file):
# add file
items.append(MultipartParam.from_file(name, value.name))
else:
items.append(MultipartParam(name, value))
datagen, headers = multipart_encode(items)
request = urllib2.Request(url, datagen, headers)
else:
request = urllib2.Request(url=url)

if ajax:
request.add_header('X_REQUESTED_WITH', 'XMLHttpRequest')
try:
# return urllib2.urlopen(request)
return self.opener.open(request)
except urllib2.HTTPError as ex:
if not debug:
raise
logger.error('error in request to %s' % path)
logger.error(ex.reason)
logger.error(ex.read())
raise

def get(self, path, debug=True):
return self.make_request(path, debug=debug)

def login(self):
""" Method to login the GeoNode site"""
self.csrf_token = self.get_csrf_token()
params = {'csrfmiddlewaretoken': self.csrf_token,
'username': self.user,
'next': '/',
'password': self.passwd}
self.make_request(
reverse('account_login'),
data=params
)
self.csrf_token = self.get_csrf_token()

def upload_file(self, _file):
""" function that uploads a file, or a collection of files, to
the GeoNode"""
if not self.csrf_token:
self.login()
spatial_files = ("dbf_file", "shx_file", "prj_file")

base, ext = os.path.splitext(_file)
params = {
# make public since wms client doesn't do authentication
'permissions': '{ "users": {"AnonymousUser": ["view_resourcebase"]} , "groups":{}}',
'csrfmiddlewaretoken': self.csrf_token
}

# deal with shapefiles
if ext.lower() == '.shp':
for spatial_file in spatial_files:
ext, _ = spatial_file.split('_')
file_path = base + '.' + ext
# sometimes a shapefile is missing an extra file,
# allow for that
if os.path.exists(file_path):
params[spatial_file] = open(file_path, 'rb')

base_file = open(_file, 'rb')
params['base_file'] = base_file
resp = self.make_request(
upload_step(),
data=params,
ajax=True)
data = resp.read()
try:
return resp, json.loads(data)
except ValueError:
# raise ValueError(
# 'probably not json, status %s' %
# resp.getcode(),
# data)
return resp, data

def get_html(self, path, debug=True):
""" Method that make a get request and passes the results to bs4
Takes a path and returns a tuple
"""
resp = self.get(path, debug)
return resp, BeautifulSoup(resp.read())

def get_json(self, path):
resp = self.get(path)
return resp, json.loads(resp.read())

def get_csrf_token(self, last=False):
"""Get a csrf_token from the home page or read from the cookie jar
based on the last response
"""
if not last:
self.get('/')
csrf = [c for c in self.cookies.cookiejar if c.name == 'csrftoken']
return csrf[0].value if csrf else None


def get_web_page(url, username=None, password=None, login_url=None):
"""Get url page possible with username and password.
Expand Down
203 changes: 6 additions & 197 deletions geonode/upload/tests/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,23 @@
from geonode.tests.base import GeoNodeBaseTestSupport

import os.path
from bs4 import BeautifulSoup
from django.conf import settings
from django.core.urlresolvers import reverse

from geonode.layers.models import Layer
from geonode.maps.models import Map
from geonode.layers.models import Layer
from geonode.upload.models import Upload
from geonode.people.models import Profile
from geonode.documents.models import Document

from geonode.people.models import Profile
from geonode.upload.models import Upload
from geonode.tests.utils import upload_step, Client
from geonode.upload.utils import _ALLOW_TIME_STEP
from geonode.geoserver.helpers import ogc_server_settings
from geonode.geoserver.helpers import cascading_delete
from geonode.geoserver.helpers import ogc_server_settings, cascading_delete
from geonode.geoserver.signals import gs_catalog

from geoserver.catalog import Catalog
from gisdata import BAD_DATA
from gisdata import GOOD_DATA
from owslib.wms import WebMapService
from poster.encode import multipart_encode, MultipartParam
from poster.streaminghttp import register_openers
# from urllib2 import HTTPError
from zipfile import ZipFile

import re
Expand All @@ -59,13 +55,10 @@
import glob
import time
import json
# import signal
import urllib
import urllib2
import logging
import tempfile
import unittest
# import subprocess
import dj_database_url

GEONODE_USER = 'admin'
Expand All @@ -86,11 +79,6 @@
Layer.objects.filter(owner=u).delete()


def upload_step(step=None):
step = reverse('data_upload', args=[step] if step else [])
return step


def get_wms(version='1.1.1', type_name=None, username=None, password=None):
""" Function to return an OWSLib WMS object """
# right now owslib does not support auth for get caps
Expand All @@ -112,195 +100,16 @@ def get_wms(version='1.1.1', type_name=None, username=None, password=None):
return WebMapService(url)


class Client(object):

"""client for making http requests"""

def __init__(self, url, user, passwd):
self.url = url
self.user = user
self.passwd = passwd
self.csrf_token = None
self.opener = self._init_url_opener()

def _init_url_opener(self):
self.cookies = urllib2.HTTPCookieProcessor()
opener = register_openers()
opener.add_handler(self.cookies) # Add cookie handler
return opener

def make_request(self, path, data=None,
ajax=False, debug=True):
url = path if path.startswith("http") else self.url + path
if ajax:
url += '&ajax=true' if '?' in url else '?ajax=true'
request = None
if data:
items = []
# wrap post parameters
for name, value in data.items():
if isinstance(value, file):
# add file
items.append(MultipartParam.from_file(name, value.name))
else:
items.append(MultipartParam(name, value))
datagen, headers = multipart_encode(items)
request = urllib2.Request(url, datagen, headers)
else:
request = urllib2.Request(url=url)

if ajax:
request.add_header('X_REQUESTED_WITH', 'XMLHttpRequest')
try:
# return urllib2.urlopen(request)
return self.opener.open(request)
except urllib2.HTTPError as ex:
if not debug:
raise
logger.error('error in request to %s' % path)
logger.error(ex.reason)
logger.error(ex.read())
raise

def get(self, path, debug=True):
return self.make_request(path, debug=debug)

def login(self):
""" Method to login the GeoNode site"""
self.csrf_token = self.get_csrf_token()
params = {'csrfmiddlewaretoken': self.csrf_token,
'username': self.user,
'next': '/',
'password': self.passwd}
self.make_request(
reverse('account_login'),
data=params
)
self.csrf_token = self.get_csrf_token()

def upload_file(self, _file):
""" function that uploads a file, or a collection of files, to
the GeoNode"""
if not self.csrf_token:
self.login()
spatial_files = ("dbf_file", "shx_file", "prj_file")

base, ext = os.path.splitext(_file)
params = {
# make public since wms client doesn't do authentication
'permissions': '{ "users": {"AnonymousUser": ["view_resourcebase"]} , "groups":{}}',
'csrfmiddlewaretoken': self.csrf_token
}

# deal with shapefiles
if ext.lower() == '.shp':
for spatial_file in spatial_files:
ext, _ = spatial_file.split('_')
file_path = base + '.' + ext
# sometimes a shapefile is missing an extra file,
# allow for that
if os.path.exists(file_path):
params[spatial_file] = open(file_path, 'rb')

base_file = open(_file, 'rb')
params['base_file'] = base_file
resp = self.make_request(
upload_step(),
data=params,
ajax=True)
data = resp.read()
try:
return resp, json.loads(data)
except ValueError:
# raise ValueError(
# 'probably not json, status %s' %
# resp.getcode(),
# data)
return resp, data

def get_html(self, path, debug=True):
""" Method that make a get request and passes the results to bs4
Takes a path and returns a tuple
"""
resp = self.get(path, debug)
return resp, BeautifulSoup(resp.read())

def get_json(self, path):
resp = self.get(path)
return resp, json.loads(resp.read())

def get_csrf_token(self, last=False):
"""Get a csrf_token from the home page or read from the cookie jar
based on the last response
"""
if not last:
self.get('/')
csrf = [c for c in self.cookies.cookiejar if c.name == 'csrftoken']
return csrf[0].value if csrf else None


class UploaderBase(GeoNodeBaseTestSupport):

settings_overrides = []

@classmethod
def setUpClass(cls):
# super(UploaderBase, cls).setUpClass()

# make a test_settings module that will apply our overrides
# test_settings = ['from geonode.settings import *']
# using_test_settings = os.getenv('DJANGO_SETTINGS_MODULE') == 'geonode.upload.tests.test_settings'
# if using_test_settings:
# test_settings.append(
# 'from geonode.upload.tests.test_settings import *')
# for so in cls.settings_overrides:
# test_settings.append('%s=%s' % so)
# with open('integration_settings.py', 'w') as fp:
# fp.write('\n'.join(test_settings))
#
# # runserver with settings
# args = [
# 'python',
# 'manage.py',
# 'runserver',
# '--settings=integration_settings',
# '--verbosity=0']
# # see for details regarding os.setsid:
# # http://www.doughellmann.com/PyMOTW/subprocess/#process-groups-sessions
# cls._runserver = subprocess.Popen(
# args,
# preexec_fn=os.setsid)

# await startup
# cl = Client(
# GEONODE_URL, GEONODE_USER, GEONODE_PASSWD
# )
# for i in range(10):
# time.sleep(.2)
# try:
# cl.get_html('/', debug=False)
# break
# except:
# pass
# if cls._runserver.poll() is not None:
# raise Exception("Error starting server, check test.log")
#
# cls.client = Client(
# GEONODE_URL, GEONODE_USER, GEONODE_PASSWD
# )
# cls.catalog = Catalog(
# GEOSERVER_URL + 'rest', GEOSERVER_USER, GEOSERVER_PASSWD
# )
pass

@classmethod
def tearDownClass(cls):
# super(UploaderBase, cls).tearDownClass()

# kill server process group
# if cls._runserver.pid:
# os.killpg(cls._runserver.pid, signal.SIGKILL)

if os.path.exists('integration_settings.py'):
os.unlink('integration_settings.py')

Expand Down
82 changes: 66 additions & 16 deletions geonode/upload/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
os.path.join(PROJECT_ROOT, "static_root")
)

# SECRET_KEY = '************************'
SECRET_KEY = 'qM-??jCGzC46L$wd'

SITEURL = "http://localhost:8000/"

Expand All @@ -44,8 +44,16 @@

ALLOWED_HOSTS = ['localhost', 'geonode', 'django', 'geonode.example.com']

# TIME_ZONE = 'Europe/Paris'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend'
)

SESSION_EXPIRED_CONTROL_ENABLED = False
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'

# Backend
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
Expand All @@ -55,16 +63,40 @@
'HOST': 'localhost',
'PORT': '5432',
'CONN_TOUT': 900,
},
# vector datastore for uploads
'datastore': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'upload_test',
'USER': 'geonode',
'PASSWORD': 'geonode',
'HOST': 'localhost',
'PORT': '5432',
'CONN_TOUT': 900,
}
}

GEOSERVER_LOCATION = os.getenv(
'GEOSERVER_LOCATION', 'http://localhost:8080/geoserver/'
)

GEOSERVER_PUBLIC_HOST = os.getenv(
'GEOSERVER_PUBLIC_HOST', SITE_HOST_NAME
)

GEOSERVER_PUBLIC_PORT = os.getenv(
'GEOSERVER_PUBLIC_PORT', 8080
)

_default_public_location = 'http://{}:{}/geoserver/'.format(
GEOSERVER_PUBLIC_HOST, GEOSERVER_PUBLIC_PORT) if GEOSERVER_PUBLIC_PORT else 'http://{}/geoserver/'.format(GEOSERVER_PUBLIC_HOST)

GEOSERVER_WEB_UI_LOCATION = os.getenv(
'GEOSERVER_WEB_UI_LOCATION', GEOSERVER_LOCATION
)

GEOSERVER_PUBLIC_LOCATION = os.getenv(
# 'GEOSERVER_PUBLIC_LOCATION', '{}geoserver/'.format(SITEURL)
'GEOSERVER_PUBLIC_LOCATION', GEOSERVER_LOCATION
'GEOSERVER_PUBLIC_LOCATION', _default_public_location
)

OGC_SERVER_DEFAULT_USER = os.getenv(
Expand All @@ -80,6 +112,7 @@
'default': {
'BACKEND': 'geonode.geoserver',
'LOCATION': GEOSERVER_LOCATION,
'WEB_UI_LOCATION': GEOSERVER_WEB_UI_LOCATION,
'LOGIN_ENDPOINT': 'j_spring_oauth2_geonode_login',
'LOGOUT_ENDPOINT': 'j_spring_oauth2_geonode_logout',
# PUBLIC_LOCATION needs to be kept like this because in dev mode
Expand All @@ -96,21 +129,16 @@
'BACKEND_WRITE_ENABLED': True,
'WPS_ENABLED': False,
'LOG_FILE': '%s/geoserver/data/logs/geoserver.log' % os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir)),
# Set to dictionary identifier of database containing spatial data in
# DATABASES dictionary to enable
'DATASTORE': 'default',
'TIMEOUT': 10 # number of seconds to allow for HTTP requests
# Set to dictionary identifier of database containing spatial data in DATABASES dictionary to enable
'DATASTORE': 'datastore',
'TIMEOUT': int(os.getenv('OGC_REQUEST_TIMEOUT', '5')),
'MAX_RETRIES': int(os.getenv('OGC_REQUEST_MAX_RETRIES', '5')),
'BACKOFF_FACTOR': float(os.getenv('OGC_REQUEST_BACKOFF_FACTOR', '0.3')),
'POOL_MAXSIZE': int(os.getenv('OGC_REQUEST_POOL_MAXSIZE', '10')),
'POOL_CONNECTIONS': int(os.getenv('OGC_REQUEST_POOL_CONNECTIONS', '10')),
}
}

# WARNING: Map Editing is affected by this. GeoExt Configuration is cached for 5 minutes
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
# 'LOCATION': '/var/tmp/django_cache',
# }
# }

# If you want to enable Mosaics use the following configuration
UPLOADER = {
# 'BACKEND': 'geonode.rest',
Expand Down Expand Up @@ -140,3 +168,25 @@
'.xml'
]
}

# Settings for MONITORING plugin
MONITORING_ENABLED = True
USER_ANALYTICS_ENABLED = True

MONITORING_CONFIG = os.getenv("MONITORING_CONFIG", None)
MONITORING_HOST_NAME = os.getenv("MONITORING_HOST_NAME", HOSTNAME)
MONITORING_SERVICE_NAME = os.getenv("MONITORING_SERVICE_NAME", 'local-geonode')

# how long monitoring data should be stored
MONITORING_DATA_TTL = timedelta(days=int(os.getenv("MONITORING_DATA_TTL", 365)))

# this will disable csrf check for notification config views,
# use with caution - for dev purpose only
MONITORING_DISABLE_CSRF = ast.literal_eval(os.environ.get('MONITORING_DISABLE_CSRF', 'False'))

if MONITORING_ENABLED:
if 'geonode.monitoring' not in INSTALLED_APPS:
INSTALLED_APPS += ('geonode.monitoring',)
if 'geonode.monitoring.middleware.MonitoringMiddleware' not in MIDDLEWARE_CLASSES:
MIDDLEWARE_CLASSES += \
('geonode.monitoring.middleware.MonitoringMiddleware',)
11 changes: 5 additions & 6 deletions geonode/upload/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
from itertools import islice
import re
import os
import json
import logging
import zipfile
import traceback

from osgeo import ogr
from lxml import etree
from itertools import islice
from defusedxml import lxml as dlxml
from osgeo import ogr

from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import ugettext as _

from geoserver.catalog import FailedRequestError, ConflictingDataError

from geonode.upload import UploadException
from geonode.utils import json_response as do_json_response, unzip_file
from geonode.geoserver.helpers import (gs_catalog,
Expand Down Expand Up @@ -104,10 +107,6 @@ def _log(msg, *args):
fixing it. Thank you!
"""

"""
JSON Responses
"""


class JSONResponse(HttpResponse):

Expand Down
5 changes: 5 additions & 0 deletions geonode/upload/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from django.views.generic import CreateView, DeleteView
from geonode.utils import fixup_shp_columnnames
from geonode.base.enumerations import CHARSETS
from geonode.monitoring import register_event
from geonode.monitoring.models import EventType

from .forms import (
LayerUploadForm,
Expand Down Expand Up @@ -567,6 +569,9 @@ def final_step_view(req, upload_session):
'success': True
}
)

register_event(req, EventType.EVENT_UPLOAD, saved_layer)

return _json_response
except LayerNotReady:
return json_response({'status': 'pending',
Expand Down
5 changes: 4 additions & 1 deletion geonode/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

from geonode import geoserver, qgis_server # noqa
from geonode.utils import check_ogc_backend
from geonode.monitoring import register_url_event

from autocomplete_light.registry import autodiscover

Expand All @@ -57,9 +58,11 @@
"map": MapSitemap
}

homepage = register_url_event()(TemplateView.as_view(template_name='index.html'))

urlpatterns = [
url(r'^$',
TemplateView.as_view(template_name='index.html'),
homepage,
name='home'),
url(r'^help/$',
TemplateView.as_view(template_name='help.html'),
Expand Down
5 changes: 0 additions & 5 deletions geonode/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,11 +906,6 @@ def resolve_object(request, model, query, permission='base.view_resourcebase',
if not allowed:
mesg = permission_msg or _('Permission Denied')
raise PermissionDenied(mesg)
if settings.MONITORING_ENABLED and obj:
if hasattr(obj, 'alternate') or obj.title:
resource_name = obj.alternate if hasattr(
obj, 'alternate') else obj.title
request.add_resource(model._meta.verbose_name_raw, resource_name)
return obj


Expand Down
16 changes: 11 additions & 5 deletions pavement.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ def test_integration(options):

name = options.get('name', 'geonode.tests.integration')
settings = options.get('settings', '')
if name == 'geonode.upload.tests.integration':
if name == 'geonode.geoserver.integration.tests':
if _django_11:
sh("cp geonode/upload/tests/test_settings.py geonode/")
settings = 'geonode.test_settings'
Expand All @@ -880,7 +880,7 @@ def test_integration(options):
call_task('start', options={'settings': settings})
call_task('setup_data', options={'settings': settings})

if name == 'geonode.upload.tests.integration':
if name == 'geonode.geoserver.integration.tests':
sh("DJANGO_SETTINGS_MODULE=%s python -W ignore manage.py makemigrations --noinput" % settings)
sh("DJANGO_SETTINGS_MODULE=%s python -W ignore manage.py migrate --noinput" % settings)
sh("DJANGO_SETTINGS_MODULE=%s python -W ignore manage.py loaddata sample_admin.json" % settings)
Expand All @@ -905,8 +905,14 @@ def test_integration(options):
live_server_option = ''

info("Running the tests now...")
sh(('%s python -W ignore manage.py test %s'
' %s --noinput %s' % (settings, name, _keepdb, live_server_option)))
if name == 'geonode.geoserver.integration.tests':
sh(('%s python -W ignore manage.py test %s'
' %s --noinput %s' % (settings, 'geonode.upload.tests.integration', _keepdb, live_server_option)))
sh(('%s python -W ignore manage.py test %s'
' %s --noinput %s' % (settings, 'geonode.monitoring.tests.integration', _keepdb, live_server_option)))
else:
sh(('%s python -W ignore manage.py test %s'
' %s --noinput %s' % (settings, name, _keepdb, live_server_option)))

except BuildFailure as e:
info('Tests failed! %s' % str(e))
Expand Down Expand Up @@ -950,7 +956,7 @@ def run_tests(options):
_backend = os.environ.get('BACKEND', OGC_SERVER['default']['BACKEND'])
if _backend == 'geonode.geoserver' and 'geonode.geoserver' in INSTALLED_APPS:
call_task('test_integration',
options={'name': 'geonode.upload.tests.integration'})
options={'name': 'geonode.geoserver.integration.tests'})
elif integration_csw_tests:
call_task('test_integration', options={'name': 'geonode.tests.csw'})

Expand Down