Source code for sortinghat.core.log

# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2021 Bitergia
#
# 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/>.
#
# Authors:
#     Miguel Ángel Fernández <mafesan@bitergia.com>
#


import json
import logging
import re
import uuid

import django.core.exceptions
import django.db.utils

from django.contrib.auth.models import User, AnonymousUser

from grimoirelab_toolkit.datetime import datetime_utcnow

from .context import SortingHatContext
from .errors import AlreadyExistsError, ClosedTransactionError
from .models import (Operation,
                     Transaction)

from .utils import validate_field


logger = logging.getLogger(__name__)


[docs]class TransactionsLog: """Class for logging transactions and operations related with the database. Every object of this class is created using the `open` class method receiving as a parameter the name of the method opening the transaction, which creates a `Transaction` object in the database. If the context was created by a job, the logger will add the job identifier to the name of the transaction. The method `log_operation` creates a new `Operation` objects linked to the transaction which was generated when the object was instanced. This method receives, among other parameters, the arguments from the method logging the operation in a Python dict object which will be converted to a serialized JSON. The `close` method adds a `closed_at` timestamp (before calling this method, this field has a `NULL` value in the DB) and it also sets to `True` the `is_closed` flag. :param trx: Transaction object generated with the class method `open` :raises ClosedTransactionError: When trying to log an operation on a closed transaction :raises TypeError: When the `op_type` is not an instance of `Operation.OpType` class """ def __init__(self, trx, ctx): self.trx = trx self.ctx = ctx
[docs] @classmethod def open(cls, name, ctx): """Create a new transaction object and save it into the DB. When the context was created by a job, this method will add the job identifier as a suffix to the name of the transaction. :param name: mame of the method opening the transaction :param ctx: context from the method opening the transaction :returns: a new `TransactionsLog` object containing the generated `Transaction` object """ # Check if input values are valid validate_field('name', name) if not isinstance(ctx, SortingHatContext): msg = "ctx value must be a SortingHatContext; {} given".format(ctx.__class__.__name__) raise TypeError(msg) if not isinstance(ctx.user, (User, AnonymousUser)): msg = "ctx.user must be a Django User or AnonymousUser; {} given".format(ctx.__class__.__name__) raise TypeError(msg) trx_name = name if ctx.job_id: trx_name += '-' + str(ctx.job_id) tuid = uuid.uuid4().hex username = None if not isinstance(ctx.user, AnonymousUser): username = ctx.user.username validate_field('username', username) trx = Transaction(tuid=tuid, name=trx_name, created_at=datetime_utcnow(), authored_by=username) try: trx.save(force_insert=True) except django.db.utils.IntegrityError as exc: _handle_integrity_error(Transaction, exc, tuid) logger.debug( f"Transaction {trx.tuid} started; " f"name='{trx.name}' author='{trx.authored_by}'" ) return cls(trx, ctx)
[docs] def close(self): """Close a given transaction adding a timestamp as closing date and setting a flag""" self.trx.closed_at = datetime_utcnow() self.trx.is_closed = True try: self.trx.save() except django.db.utils.IntegrityError as exc: _handle_integrity_error(Transaction, exc, self.trx.tuid) logger.debug(f"Transaction {self.trx.tuid} finished")
[docs] def log_operation(self, op_type, entity_type, timestamp, args, target): """Create a new operation object and save it into the DB. :param op_type: Type of the operation which is recorded (ADD, DELETE, UPDATE) :param entity_type: Type of entity involved in the operations (UUID, ENROLLMENT, etc.) :param timestamp: Datetime when the operation is created :param args: Input arguments from the method creating the operation :param target: Argument which the operation is directed to :raises ClosedTransactionError: When trying to log an operation on a closed transaction :raises TypeError: When the `op_type` is not an instance of `Operation.OpType` class :returns: a new Operation object """ if self.trx.is_closed: msg = 'Log operation not allowed, transaction {} is already closed'.format(self.trx.tuid) raise ClosedTransactionError(msg=msg) # Check if input values are valid validate_field('entity_type', entity_type) validate_field('target', target) if not isinstance(op_type, Operation.OpType): msg = "'op_type' value must be a 'Operation.OpType'; {} given".format(op_type.__class__.__name__) raise TypeError(msg) args_dump = json.dumps(args) ouid = uuid.uuid4().hex operation = Operation(ouid=ouid, trx=self.trx, op_type=op_type, target=target, entity_type=entity_type, timestamp=timestamp, args=args_dump) try: operation.save(force_insert=True) except django.db.utils.IntegrityError as exc: _handle_integrity_error(Operation, exc, self.trx.tuid) logger.debug( f"Operation {operation.ouid} completed; " f"trx='{operation.trx.tuid}' op='{operation.op_type}' " f"type='{entity_type}' target='{target}' args={args};" ) return operation
_MYSQL_DUPLICATE_ENTRY_ERROR_REGEX = re.compile(r"Duplicate entry '(?P<value>.+)' for key") def _handle_integrity_error(model, exc, tuid): """Handle integrity error internal logging exceptions""" logger.error(f"Transaction {tuid} aborted; integrity error;", exc_info=True) m = re.match(_MYSQL_DUPLICATE_ENTRY_ERROR_REGEX, exc.__cause__.args[1]) if not m: raise exc entity = model.__name__ eid = m.group('value') raise AlreadyExistsError(entity=entity, eid=eid)