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}