diff --git a/settings/common.py b/settings/common.py index 7096f6e6..9128a013 100644 --- a/settings/common.py +++ b/settings/common.py @@ -358,7 +358,17 @@ LOGGING = { "level": "ERROR", "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler", - } + }, + 'logstash': { + 'level': 'INFO', + 'class': 'logstash.TCPLogstashHandler', + 'host': 'localhost', + 'port': 5000, + 'version': 1, # Version of logstash event schema. Default value: 0 (for backward compatibility of the library) + 'message_type': 'taiga-bi', # 'type' field in logstash message. Default value: 'logstash'. + # 'fqdn': False, # Fully qualified domain name. Default value: false. + #'tags': ['tag1', 'tag2'], # list of tags. Default: None. + }, }, "loggers": { "django": { @@ -380,6 +390,11 @@ LOGGING = { "handlers": ["console"], "level": "DEBUG", "propagate": False, + }, + "bi": { + "handlers": ["logstash"], + "level": "INFO", + "propagate": False, } } } diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index e07635ed..9f8a2e3b 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -49,6 +49,7 @@ from django.db import transaction as tx from django.utils.translation import ugettext as _ from taiga.base import response +from taiga.base.logger import bilogger from .settings import api_settings from .utils import get_object_or_404 @@ -97,7 +98,9 @@ class CreateModelMixin: self.pre_conditions_on_save(serializer.object) self.object = serializer.save(force_insert=True) self.post_save(self.object, created=True) + headers = self.get_success_headers(serializer.data) + bilogger(request, self, obj=self.object) return response.Created(serializer.data, headers=headers) return response.BadRequest(serializer.errors) @@ -136,6 +139,7 @@ class ListModelMixin: else: serializer = self.get_serializer(self.object_list, many=True) + bilogger(request, self) return response.Ok(serializer.data) @@ -152,6 +156,7 @@ class RetrieveModelMixin: raise Http404 serializer = self.get_serializer(self.object) + bilogger(request, self, obj=self.object) return response.Ok(serializer.data) @@ -189,6 +194,7 @@ class UpdateModelMixin: self.object = serializer.save(force_update=True) self.post_save(self.object, created=False) + bilogger(request, self, obj=self.object) return response.Ok(serializer.data) def partial_update(self, request, *args, **kwargs): @@ -238,6 +244,7 @@ class DestroyModelMixin: self.pre_conditions_on_delete(obj) obj.delete() self.post_delete(obj) + bilogger(request, self, obj=obj) return response.NoContent() diff --git a/taiga/base/logger.py b/taiga/base/logger.py new file mode 100644 index 00000000..9e0ceea7 --- /dev/null +++ b/taiga/base/logger.py @@ -0,0 +1,30 @@ +import logging +import copy +from taiga.base.utils import json +bi_logger = logging.getLogger("bi") + + +def bilogger(request, view, obj=None, **kwargs): + data = copy.copy(kwargs) + + data["ip"] = request.META.get("REMOTE_ADDR", None) + data["user-agent"] = request.META.get("HTTP_USER_AGENT", None) + data["path"] = request.get_full_path() + + if "success" not in kwargs: + data["success"] = True + + data['user'] = 'anon' + if not request.user.is_anonymous(): + data['user'] = request.user.id + data['view'] = view.get_view_name() + + if "action" not in kwargs: + data['action'] = view.action + + if obj is not None: + data['object-id'] = obj.id + if hasattr(obj, 'project_id'): + data['project-id'] = obj.project_id + + bi_logger.info("", extra=data) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 50fe5f46..07db6998 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -59,6 +59,8 @@ from . import permissions from . import serializers from . import services +from taiga.base.logger import bilogger + ###################################################### ## Project @@ -171,6 +173,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.object.save(update_fields=["logo"]) serializer = self.get_serializer(self.object) + bilogger(request, self, obj=self.object) return response.Ok(serializer.data) @detail_route(methods=["POST"]) @@ -185,6 +188,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.object.save(update_fields=["logo"]) serializer = self.get_serializer(self.object) + bilogger(request, self, obj=self.object) return response.Ok(serializer.data) @detail_route(methods=["POST"]) @@ -194,6 +198,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.pre_conditions_on_save(project) notify_level = request.DATA.get("notify_level", NotifyLevel.involved) project.add_watcher(self.request.user, notify_level=notify_level) + bilogger(request, self, obj=project) return response.Ok() @detail_route(methods=["POST"]) @@ -203,6 +208,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.pre_conditions_on_save(project) user = self.request.user project.remove_watcher(user) + bilogger(request, self, obj=project) return response.Ok() @list_route(methods=["POST"]) @@ -216,6 +222,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, data = serializer.data services.update_projects_order_in_bulk(data, "user_order", request.user) + bilogger(request, self) return response.NoContent(data=None) @detail_route(methods=["POST"]) @@ -223,16 +230,18 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, template_name = request.DATA.get('template_name', None) template_description = request.DATA.get('template_description', None) + project = self.get_object() + if not template_name: + bilogger(request, self, obj=project, success=False, error="invalid-template-name") raise response.BadRequest(_("Not valid template name")) if not template_description: + bilogger(request, self, obj=project, success=False, error="invalid-template-description") raise response.BadRequest(_("Not valid template description")) template_slug = slugify_uniquely(template_name, models.ProjectTemplate) - project = self.get_object() - self.check_permissions(request, 'create_template', project) template = models.ProjectTemplate( @@ -243,6 +252,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, template.load_data_from_project(project) template.save() + bilogger(request, self, obj=project) return response.Created(serializers.ProjectTemplateSerializer(template).data) @detail_route(methods=['POST']) @@ -251,6 +261,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, 'leave', project) self.pre_conditions_on_save(project) services.remove_user_from_project(request.user, project) + bilogger(request, self, obj=project) return response.Ok() @detail_route(methods=["POST"]) @@ -259,6 +270,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "regenerate_userstories_csv_uuid", project) self.pre_conditions_on_save(project) data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")} + bilogger(request, self, obj=project) return response.Ok(data) @detail_route(methods=["POST"]) @@ -267,6 +279,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "regenerate_issues_csv_uuid", project) self.pre_conditions_on_save(project) data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} + bilogger(request, self, obj=project) return response.Ok(data) @detail_route(methods=["POST"]) @@ -275,6 +288,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "regenerate_tasks_csv_uuid", project) self.pre_conditions_on_save(project) data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} + bilogger(request, self, obj=project) return response.Ok(data) @list_route(methods=["GET"]) @@ -290,18 +304,21 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, modules_config = services.get_modules_config(project) if request.method == "GET": + bilogger(request, self, action="modules-get", obj=project) return response.Ok(modules_config.config) else: self.pre_conditions_on_save(project) modules_config.config.update(request.DATA) modules_config.save() + bilogger(request, self, action="modules-update", obj=project) return response.NoContent() @detail_route(methods=["GET"]) def stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "stats", project) + bilogger(request, self, id=project.id) return response.Ok(services.get_stats_for_project(project)) def _regenerate_csv_uuid(self, project, field): @@ -314,18 +331,21 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, def member_stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "member_stats", project) + bilogger(request, self, obj=project) return response.Ok(services.get_member_stats_for_project(project)) @detail_route(methods=["GET"]) def issues_stats(self, request, pk=None): project = self.get_object() self.check_permissions(request, "issues_stats", project) + bilogger(request, self, obj=project) return response.Ok(services.get_stats_for_project_issues(project)) @detail_route(methods=["GET"]) def tags_colors(self, request, pk=None): project = self.get_object() self.check_permissions(request, "tags_colors", project) + bilogger(request, self, obj=project) return response.Ok(dict(project.tags_colors)) @detail_route(methods=["POST"]) @@ -347,7 +367,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, user_model = apps.get_model("users", "User") try: user = user_model.objects.get(id=user_id) - except user_model.DoesNotExist: return response.BadRequest(_("The user doesn't exist")) @@ -390,6 +409,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, raise exc.WrongArguments(_("Invalid token")) project = self.get_object() + self.check_permissions(request, "transfer_reject", project) reason = request.DATA.get('reason', None) @@ -432,7 +452,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.pre_delete(obj) self.pre_conditions_on_delete(obj) - obj.delete_related_content() + obj.delete_related_content() obj.delete() self.post_delete(obj) return response.NoContent()