diff --git a/warden_server/warden_server.py b/warden_server/warden_server.py index e52e06b2e25635103b1d2a49cf2c0a52acfdf4df..6c1b86b899c8adf29e911dd106dd8dbdb3b82b1e 100755 --- a/warden_server/warden_server.py +++ b/warden_server/warden_server.py @@ -51,94 +51,134 @@ class Encoder(json.JSONEncoder): def default(self, o): if isinstance(o, Error): return o.to_dict() + if isinstance(o, ErrorMessage): + out = o.other_args.copy() + out.pop("exc", None) + out["error"] = o.error + out["message"] = o.message + if o.events: + out["events"] = list(o.events) + return out return str(o) + +class ErrorMessage(Exception): + + def __init__(self, error, message, events=None, unique_id=None, **kwargs): + super(Exception, self).__setattr__("error", error) + super(Exception, self).__setattr__("message", message) + super(Exception, self).__setattr__("unique_id", unique_id) + self.events = set() if events is None else set(events) + self.other_args = kwargs + + def __repr__(self): + return "%s(error=%d, message=%s)" % ( + type(self).__name__, self.error, repr(self.message) + ) + + def __str__(self): + if sys.version_info[0] < 3: + return self.str_err().encode('ascii', 'backslashereplace') + return self.str_err() + + def str_err(self): + exc = self.other_args.get("exc", None) + if exc in (None, (None, None, None)): + exc_cause = "" + else: + exc_cause = " (cause was %s: %s)" % (exc[0].__name__, str(exc[1])) + return "Error(%s) %s%s" % (self.error, self.message, exc_cause) + + def str_info(self): + arg_copy = self.other_args.copy() + arg_copy.pop("req_id", None) + arg_copy.pop("method", None) + arg_copy.pop("exc", None) + if arg_copy: + return "Detail: %s" % json.dumps(arg_copy, cls=Encoder) + return "" + + def str_debug(self): + exc = self.other_args.get("exc", None) + if exc in (None, (None, None, None)): + return "" + exc_tb = exc[2] + if not exc_tb: + return "" + + return "Traceback:\n" + "".join(format_tb(exc_tb)) + + def __getattr__(self, name): + if name in self.other_args: + return self.other_args[name] + raise AttributeError + + def __setattr__(self, name, value): + if name in ("events", "exc", "other_args"): + super(Exception, self).__setattr__(name, value) + return + if name in ("error", "message", "unique_id"): + raise AttributeError("Cannot change the attribute %s" % name) + self.other_args[name] = value + + class Error(Exception): def __init__(self, method=None, req_id=None, errors=None, **kwargs): self.method = method self.req_id = req_id - self.errors = [kwargs] if kwargs else [] + if "message" in kwargs: + kwargs.setdefault("error", 500) + self.errors = [ErrorMessage(**kwargs)] + else: + self.errors = [] if errors: self.errors.extend(errors) def append(self, _events=None, **kwargs): - self.errors.append(kwargs) + kwargs.setdefault("message", "No further information") + kwargs.setdefault("error", 500) + self.errors.append(ErrorMessage(**kwargs)) def get_http_err_msg(self): try: - err = self.errors[0]["error"] - msg = self.errors[0]["message"].replace("\n", " ") - except (IndexError, KeyError): + err = self.errors[0].error + msg = self.errors[0].message + except (IndexError, AttributeError): err = 500 msg = "There's NO self-destruction button! Ah, you've just found it..." - for e in self.errors: - next_err = e.get("error", 500) - if err != next_err: - # errors not same, round to basic err code (400, 500) - # and use the highest one - err = max(err//100, next_err//100)*100 - next_msg = e.get("message", "Unknown error").replace("\n", " ") - if msg != next_msg: - msg = "Multiple errors" + return err, msg + + if not all(msg == e.message for e in self.errors): + # messages not the same, get Multiple errors + msg = "Multiple errors" + if not all(err == e.error for e in self.errors): + # errors not same, round to basic err code (400, 500) + # and use the highest one + err = max(e.error for e in self.errors) // 100 * 100 msg = "".join((c if '\x20' <= c != '\x7f' else r'\x{:02x}'.format(ord(c))) for c in msg) # escape control characters return err, msg def __str__(self): - return "\n".join(self.str_err(e) for e in self.errors) + return "\n".join(str(e) for e in self.errors) def log(self, logger, prio=logging.ERROR): for e in self.errors: - logger.log(prio, self.str_err(e)) - info = self.str_info(e) + logger.log(prio, e.str_err()) + info = e.str_info() if info: logger.info(info) - debug = self.str_debug(e) + debug = e.str_debug() if debug: logger.debug(debug) - def str_err(self, e): - out = [] - out.append("Error(%s) %s " % (e.get("error", 0), e.get("message", "Unknown error"))) - if "exc" in e and e["exc"]: - out.append("(cause was %s: %s)" % (e["exc"][0].__name__, str(e["exc"][1]))) - return "".join(out) - - def str_info(self, e): - ecopy = dict(e) # shallow copy - ecopy.pop("req_id", None) - ecopy.pop("method", None) - ecopy.pop("error", None) - ecopy.pop("message", None) - ecopy.pop("exc", None) - if ecopy: - out = "Detail: %s" % (json.dumps(ecopy, cls=Encoder)) - else: - out = "" - return out - - def str_debug(self, e): - out = [] - if not e.get("exc"): - return "" - exc_tb = e["exc"][2] - if exc_tb: - out.append("Traceback:\n") - out.extend(format_tb(exc_tb)) - return "".join(out) - def to_dict(self): - errlist = [] - for e in self.errors: - ecopy = dict(e) - ecopy.pop("exc", None) - errlist.append(ecopy) d = { "method": self.method, "req_id": self.req_id, - "errors": errlist + "errors": self.errors } return d @@ -466,14 +506,15 @@ class JSONSchemaValidator(NoValidator): res = [] for error in sorted(self.validator.iter_errors(event), key=sortkey): - res.append({ - "error": 460, - "message": "Validation error: key \"%s\", value \"%s\"" % ( - "/".join(str(v) for v in error.path), - error.instance - ), - "expected": error.schema.get('description', 'no additional info') - }) + res.append( + ErrorMessage( + 460, "Validation error: key \"%s\", value \"%s\"" % ( + "/".join(map(str, error.path)), + error.instance + ), + expected=error.schema.get('description', 'no additional info') + ) + ) return res @@ -806,7 +847,7 @@ class DataBase(ObjectBase): except Exception as e: exception = self.req.error(message="DB error", error=500, exc=sys.exc_info(), env=self.req.env) exception.log(self.log) - return [{"error": 500, "message": "DB error %s" % type(e).__name__}] + return [ErrorMessage(500, "DB error %s" % type(e).__name__)] @abc.abstractmethod def _build_insert_last_received_id(self, client, id): @@ -1688,28 +1729,23 @@ class WardenHandler(ObjectBase): return res - def check_node(self, event, name): + def check_node(self, event, event_indx, name): try: ev_id = event['Node'][0]['Name'].lower() except (KeyError, TypeError, IndexError): # Event does not bear valid Node attribute - return [{"error": 422, "message": "Event does not bear valid Node attribute"}] + return [ + ErrorMessage(422, "Event does not bear valid Node attribute", {event_indx}) + ] if ev_id != name: - return [{"error": 422, "message": "Node does not correspond with saving client"}] + return [ + ErrorMessage(422, "Node does not correspond with saving client", {event_indx}) + ] return [] - def add_event_nums(self, ilist, events, errlist): - for err in errlist: - err.setdefault("events", []).extend(ilist) - ev_ids = err.setdefault("events_id", []) - for i in ilist: - event = events[i] - try: - id = event["ID"] - except (KeyError, TypeError, ValueError): - id = None - ev_ids.append(id) - return errlist + def add_errors(self, errs_to_add): + for err in errs_to_add: + self.errs.setdefault((err.error, err.message, err.unique_id), err).events.update(err.events) @expose(write=True) @json_wrapper @@ -1717,40 +1753,55 @@ class WardenHandler(ObjectBase): if not isinstance(events, list): raise self.req.error(message="List of events expected.", error=400) - errs = [] + self.errs = {} if len(events) > self.send_events_limit: - errs.extend(self.add_event_nums(range(self.send_events_limit, len(events)), events, [ - {"error": 507, "message": "Too much events in one batch.", "send_events_limit": self.send_events_limit}])) + self.add_errors( + [ + ErrorMessage( + 507, "Too many events in one batch.", + set(range(self.send_events_limit, len(events))), + send_events_limit=self.send_events_limit + ) + ] + ) - saved = 0 events_tosend = [] events_raw = [] events_nums = [] for i, event in enumerate(events[0:self.send_events_limit]): v_errs = self.validator.check(event) if v_errs: - errs.extend(self.add_event_nums([i], events, v_errs)) + self.add_errors(v_errs) continue - node_errs = self.check_node(event, self.req.client.name) + node_errs = self.check_node(event, i, self.req.client.name) if node_errs: - errs.extend(self.add_event_nums([i], events, node_errs)) + self.add_errors(node_errs) continue if self.req.client.test and 'Test' not in event.get('Category', []): - errs.extend( - self.add_event_nums([i], events, [{ - "error": 422, - "message": "You're allowed to send only messages, containing \"Test\" among categories.", - "categories": event.get('Category', [])}])) + self.add_errors( + [ + ErrorMessage( + 422, "You're allowed to send only messages containing \"Test\" among categories.", {i}, + # Ensure that 1the error message is contained for every combination of categories + unique_id=tuple(event.get('Category', [])), + categories=event.get('Category', []) + ) + ] + ) continue raw_event = json.dumps(event) if len(raw_event) >= self.db.event_size_limit: - errs.extend( - self.add_event_nums([i], events, [ - {"error": 413, "message": "Event too long (>%i B)" % self.db.event_size_limit} - ])) + self.add_errors( + [ + ErrorMessage( + 413, "Event too long (>%i B)" % self.db.event_size_limit, {i}, + event_size_limit = self.db.event_size_limit + ) + ] + ) continue events_tosend.append(event) @@ -1758,15 +1809,13 @@ class WardenHandler(ObjectBase): events_nums.append(i) db_errs = self.db.store_events(self.req.client, events_tosend, events_raw) - if db_errs: - errs.extend(self.add_event_nums(events_nums, events_tosend, db_errs)) - saved = 0 - else: - saved = len(events_tosend) + self.add_errors(db_errs) + + saved = 0 if db_errs else len(events_tosend) self.log.info("Saved %i events" % saved) - if errs: - raise self.req.error(errors=errs) + if self.errs: + raise self.req.error(errors=self.errs.values()) return {"saved": saved}