# -*- coding: utf-8 -*- # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import colander from colander import null # those keywords are here so we can distingush between searching for tags and # normal properties of reports/logs accepted_search_params = [ "resource", "request_id", "start_date", "end_date", "page", "min_occurences", "http_status", "priority", "error", "url_path", "url_domain", "report_status", "min_duration", "max_duration", "message", "level", "namespace", ] @colander.deferred def deferred_utcnow(node, kw): return kw["utcnow"] @colander.deferred def optional_limited_date(node, kw): if not kw.get("allow_permanent_storage"): return limited_date def lowercase_preparer(input_data): """ Transforms a list of string entries to lowercase Used in search query validation """ if not input_data: return input_data return [x.lower() for x in input_data] def shortener_factory(cutoff_size=32): """ Limits the input data to specific character count :arg cutoff_cutoff_size How much characters to store """ def shortener(input_data): if not input_data: return input_data else: if isinstance(input_data, str): return input_data[:cutoff_size] else: return input_data return shortener def cast_to_unicode_or_null(value): if value is not colander.null: return str(value) return None class NonTZDate(colander.DateTime): """ Returns null for incorrect date format - also removes tz info""" def deserialize(self, node, cstruct): # disabled for now # if cstruct and isinstance(cstruct, str): # if ':' not in cstruct: # cstruct += ':0.0' # if '.' not in cstruct: # cstruct += '.0' value = super(NonTZDate, self).deserialize(node, cstruct) if value: return value.replace(tzinfo=None) return value class UnknownType(object): """ Universal type that will accept a deserialized JSON object and store it unaltered """ def serialize(self, node, appstruct): if appstruct is null: return null return appstruct def deserialize(self, node, cstruct): if cstruct is null: return null return cstruct def cstruct_children(self): return [] # SLOW REPORT SCHEMA def rewrite_type(input_data): """ Fix for legacy appenlight clients """ if input_data == "remote_call": return "remote" return input_data class ExtraTupleSchema(colander.TupleSchema): name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 64)) value = colander.SchemaNode( UnknownType(), preparer=shortener_factory(512), missing=None ) class ExtraSchemaList(colander.SequenceSchema): tag = ExtraTupleSchema() missing = None class TagsTupleSchema(colander.TupleSchema): name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 128)) value = colander.SchemaNode( UnknownType(), preparer=shortener_factory(128), missing=None ) class TagSchemaList(colander.SequenceSchema): tag = TagsTupleSchema() missing = None class NumericTagsTupleSchema(colander.TupleSchema): name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 128)) value = colander.SchemaNode(colander.Float(), missing=0) class NumericTagSchemaList(colander.SequenceSchema): tag = NumericTagsTupleSchema() missing = None class SlowCallSchema(colander.MappingSchema): """ Validates slow call format in slow call list """ start = colander.SchemaNode(NonTZDate()) end = colander.SchemaNode(NonTZDate()) statement = colander.SchemaNode(colander.String(), missing="") parameters = colander.SchemaNode(UnknownType(), missing=None) type = colander.SchemaNode( colander.String(), preparer=rewrite_type, validator=colander.OneOf( ["tmpl", "sql", "nosql", "remote", "unknown", "custom"] ), missing="unknown", ) subtype = colander.SchemaNode( colander.String(), validator=colander.Length(1, 16), missing="unknown" ) location = colander.SchemaNode( colander.String(), validator=colander.Length(1, 255), missing="" ) def limited_date(node, value): """ checks to make sure that the value is not older/newer than 2h """ past_hours = 72 future_hours = 2 min_time = datetime.datetime.utcnow() - datetime.timedelta(hours=past_hours) max_time = datetime.datetime.utcnow() + datetime.timedelta(hours=future_hours) if min_time > value: msg = "%r is older from current UTC time by " + str(past_hours) msg += ( " hours. Ask administrator to enable permanent logging for " "your application to store logs with dates in past." ) raise colander.Invalid(node, msg % value) if max_time < value: msg = "%r is newer from current UTC time by " + str(future_hours) msg += ( " hours. Ask administrator to enable permanent logging for " "your application to store logs with dates in future." ) raise colander.Invalid(node, msg % value) class SlowCallListSchema(colander.SequenceSchema): """ Validates list of individual slow calls """ slow_call = SlowCallSchema() class RequestStatsSchema(colander.MappingSchema): """ Validates format of requests statistics dictionary """ main = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0) sql = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0) nosql = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) remote = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) tmpl = colander.SchemaNode(colander.Float(), validator=colander.Range(0), missing=0) custom = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) sql_calls = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) nosql_calls = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) remote_calls = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) tmpl_calls = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) custom_calls = colander.SchemaNode( colander.Float(), validator=colander.Range(0), missing=0 ) class FrameInfoVarSchema(colander.SequenceSchema): """ Validates format of frame variables of a traceback """ vars = colander.SchemaNode(UnknownType(), validator=colander.Length(2, 2)) class FrameInfoSchema(colander.MappingSchema): """ Validates format of a traceback line """ cline = colander.SchemaNode(colander.String(), missing="") module = colander.SchemaNode(colander.String(), missing="") line = colander.SchemaNode(colander.String(), missing="") file = colander.SchemaNode(colander.String(), missing="") fn = colander.SchemaNode(colander.String(), missing="") vars = FrameInfoVarSchema() class FrameInfoListSchema(colander.SequenceSchema): """ Validates format of list of traceback lines """ frame = colander.SchemaNode(UnknownType()) class ReportDetailBaseSchema(colander.MappingSchema): """ Validates format of report - ie. request parameters and stats for a request in report group """ username = colander.SchemaNode( colander.String(), preparer=[shortener_factory(255), lambda x: x or ""], missing="", ) request_id = colander.SchemaNode( colander.String(), preparer=shortener_factory(40), missing="" ) url = colander.SchemaNode( colander.String(), preparer=shortener_factory(1024), missing="" ) ip = colander.SchemaNode( colander.String(), preparer=shortener_factory(39), missing=None ) start_time = colander.SchemaNode( NonTZDate(), validator=optional_limited_date, missing=deferred_utcnow ) end_time = colander.SchemaNode( NonTZDate(), validator=optional_limited_date, missing=None ) user_agent = colander.SchemaNode( colander.String(), preparer=[shortener_factory(512), lambda x: x or ""], missing="", ) message = colander.SchemaNode( colander.String(), preparer=shortener_factory(2048), missing="" ) group_string = colander.SchemaNode( colander.String(), preparer=shortener_factory(512), missing=None ) request_stats = RequestStatsSchema(missing=None) request = colander.SchemaNode(colander.Mapping(unknown="preserve"), missing={}) traceback = FrameInfoListSchema(missing=None) slow_calls = SlowCallListSchema(missing=[]) extra = ExtraSchemaList() class ReportDetailSchema_0_5(ReportDetailBaseSchema): pass class ReportDetailSchemaPermissiveDate_0_5(ReportDetailSchema_0_5): start_time = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow) end_time = colander.SchemaNode(NonTZDate(), missing=None) class ReportSchemaBase(colander.MappingSchema): """ Validates format of report group """ client = colander.SchemaNode(colander.String(), preparer=lambda x: x or "unknown") server = colander.SchemaNode( colander.String(), preparer=[lambda x: x.lower() if x else "unknown", shortener_factory(128)], missing="unknown", ) priority = colander.SchemaNode( colander.Int(), preparer=[lambda x: x or 5], validator=colander.Range(1, 10), missing=5, ) language = colander.SchemaNode(colander.String(), missing="unknown") error = colander.SchemaNode( colander.String(), preparer=shortener_factory(512), missing="" ) view_name = colander.SchemaNode( colander.String(), preparer=[shortener_factory(128), lambda x: x or ""], missing="", ) http_status = colander.SchemaNode( colander.Int(), preparer=[lambda x: x or 200], validator=colander.Range(1) ) occurences = colander.SchemaNode( colander.Int(), validator=colander.Range(1, 99999999999), missing=1 ) tags = TagSchemaList() class ReportSchema_0_5(ReportSchemaBase, ReportDetailSchema_0_5): pass class ReportSchemaPermissiveDate_0_5( ReportSchemaBase, ReportDetailSchemaPermissiveDate_0_5 ): pass class ReportListSchema_0_5(colander.SequenceSchema): """ Validates format of list of report groups """ report = ReportSchema_0_5() validator = colander.Length(1) class ReportListPermissiveDateSchema_0_5(colander.SequenceSchema): """ Validates format of list of report groups """ report = ReportSchemaPermissiveDate_0_5() validator = colander.Length(1) class LogSchema(colander.MappingSchema): """ Validates format if individual log entry """ primary_key = colander.SchemaNode( UnknownType(), preparer=[cast_to_unicode_or_null, shortener_factory(128)], missing=None, ) log_level = colander.SchemaNode( colander.String(), preparer=shortener_factory(10), missing="UNKNOWN" ) message = colander.SchemaNode( colander.String(), preparer=shortener_factory(4096), missing="" ) namespace = colander.SchemaNode( colander.String(), preparer=shortener_factory(128), missing="" ) request_id = colander.SchemaNode( colander.String(), preparer=shortener_factory(40), missing="" ) server = colander.SchemaNode( colander.String(), preparer=shortener_factory(128), missing="unknown" ) date = colander.SchemaNode( NonTZDate(), validator=limited_date, missing=deferred_utcnow ) tags = TagSchemaList() class LogSchemaPermanent(LogSchema): date = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow) permanent = colander.SchemaNode(colander.Boolean(), missing=False) class LogListSchema(colander.SequenceSchema): """ Validates format of list of log entries """ log = LogSchema() validator = colander.Length(1) class LogListPermanentSchema(colander.SequenceSchema): """ Validates format of list of log entries """ log = LogSchemaPermanent() validator = colander.Length(1) class ViewRequestStatsSchema(RequestStatsSchema): requests = colander.SchemaNode( colander.Integer(), validator=colander.Range(0), missing=0 ) class ViewMetricTupleSchema(colander.TupleSchema): """ Validates list of views and their corresponding request stats object ie: ["dir/module:func",{"custom": 0.0..}] """ view_name = colander.SchemaNode( colander.String(), preparer=[shortener_factory(128), lambda x: x or "unknown"], missing="unknown", ) metrics = ViewRequestStatsSchema() class ViewMetricListSchema(colander.SequenceSchema): """ Validates view breakdown stats objects list {metrics key of server/time object} """ view_tuple = ViewMetricTupleSchema() validator = colander.Length(1) class ViewMetricSchema(colander.MappingSchema): """ Validates server/timeinterval object, ie: {server/time object} """ timestamp = colander.SchemaNode(NonTZDate(), validator=limited_date, missing=None) server = colander.SchemaNode( colander.String(), preparer=[shortener_factory(128), lambda x: x or "unknown"], missing="unknown", ) metrics = ViewMetricListSchema() class GeneralMetricSchema(colander.MappingSchema): """ Validates universal metric schema """ namespace = colander.SchemaNode( colander.String(), missing="", preparer=shortener_factory(128) ) server_name = colander.SchemaNode( colander.String(), preparer=[shortener_factory(128), lambda x: x or "unknown"], missing="unknown", ) timestamp = colander.SchemaNode( NonTZDate(), validator=limited_date, missing=deferred_utcnow ) tags = TagSchemaList(missing=colander.required) class GeneralMetricPermanentSchema(GeneralMetricSchema): """ Validates universal metric schema """ timestamp = colander.SchemaNode(NonTZDate(), missing=deferred_utcnow) class GeneralMetricsListSchema(colander.SequenceSchema): metric = GeneralMetricSchema() validator = colander.Length(1) class GeneralMetricsPermanentListSchema(colander.SequenceSchema): metric = GeneralMetricPermanentSchema() validator = colander.Length(1) class MetricsListSchema(colander.SequenceSchema): """ Validates list of metrics objects ie: [{server/time object}, ] part """ metric = ViewMetricSchema() validator = colander.Length(1) class StringToAppList(object): """ Returns validated list of application ids from user query and set of applications user is allowed to look at transform string to list containing single integer """ def serialize(self, node, appstruct): if appstruct is null: return null return appstruct def deserialize(self, node, cstruct): if cstruct is null: return null apps = set([int(a) for a in node.bindings["resources"]]) if isinstance(cstruct, str): cstruct = [cstruct] cstruct = [int(a) for a in cstruct] valid_apps = list(apps.intersection(set(cstruct))) if valid_apps: return valid_apps return null def cstruct_children(self): return [] @colander.deferred def possible_applications_validator(node, kw): possible_apps = [int(a) for a in kw["resources"]] return colander.All(colander.ContainsOnly(possible_apps), colander.Length(1)) @colander.deferred def possible_applications(node, kw): return [int(a) for a in kw["resources"]] @colander.deferred def today_start(node, kw): return datetime.datetime.utcnow().replace(second=0, microsecond=0, minute=0, hour=0) @colander.deferred def today_end(node, kw): return datetime.datetime.utcnow().replace( second=0, microsecond=0, minute=59, hour=23 ) @colander.deferred def old_start(node, kw): t_delta = datetime.timedelta(days=90) return ( datetime.datetime.utcnow().replace(second=0, microsecond=0, minute=0, hour=0) - t_delta ) @colander.deferred def today_end(node, kw): return datetime.datetime.utcnow().replace( second=0, microsecond=0, minute=59, hour=23 ) class PermissiveDate(colander.DateTime): """ Returns null for incorrect date format - also removes tz info""" def deserialize(self, node, cstruct): if not cstruct: return null try: result = colander.iso8601.parse_date( cstruct, default_timezone=self.default_tzinfo ) except colander.iso8601.ParseError: return null return result.replace(tzinfo=None) class LogSearchSchema(colander.MappingSchema): def schema_type(self, **kw): return colander.Mapping(unknown="preserve") resource = colander.SchemaNode( StringToAppList(), validator=possible_applications_validator, missing=possible_applications, ) message = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) level = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), preparer=lowercase_preparer, missing=None, ) namespace = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), preparer=lowercase_preparer, missing=None, ) request_id = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), preparer=lowercase_preparer, missing=None, ) start_date = colander.SchemaNode(PermissiveDate(), missing=None) end_date = colander.SchemaNode(PermissiveDate(), missing=None) page = colander.SchemaNode( colander.Integer(), validator=colander.Range(min=1), missing=1 ) class ReportSearchSchema(colander.MappingSchema): def schema_type(self, **kw): return colander.Mapping(unknown="preserve") resource = colander.SchemaNode( StringToAppList(), validator=possible_applications_validator, missing=possible_applications, ) request_id = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) start_date = colander.SchemaNode(PermissiveDate(), missing=None) end_date = colander.SchemaNode(PermissiveDate(), missing=None) page = colander.SchemaNode( colander.Integer(), validator=colander.Range(min=1), missing=1 ) min_occurences = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.Integer()), missing=None, ) http_status = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.Integer()), missing=None, ) priority = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.Integer()), missing=None, ) error = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) url_path = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) url_domain = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) report_status = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String()), missing=None, ) min_duration = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.Float()), missing=None, ) max_duration = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.Float()), missing=None, ) class TagSchema(colander.MappingSchema): """ Used in log search """ name = colander.SchemaNode(colander.String(), validator=colander.Length(1, 32)) value = colander.SchemaNode( colander.Sequence(accept_scalar=True), colander.SchemaNode(colander.String(), validator=colander.Length(1, 128)), missing=None, ) op = colander.SchemaNode( colander.String(), validator=colander.Length(1, 128), missing=None ) class TagListSchema(colander.SequenceSchema): tag = TagSchema() class RuleFieldType(object): """ Validator which succeeds if the value passed to it is one of a fixed set of values """ def __init__(self, cast_to): self.cast_to = cast_to def __call__(self, node, value): try: if self.cast_to == "int": int(value) elif self.cast_to == "float": float(value) elif self.cast_to == "unicode": str(value) except: raise colander.Invalid( node, "Can't cast {} to {}".format(value, self.cast_to) ) def build_rule_schema(ruleset, check_matrix): """ Accepts ruleset and a map of fields/possible operations and builds validation class """ schema = colander.SchemaNode(colander.Mapping()) schema.add(colander.SchemaNode(colander.String(), name="field")) if ruleset["field"] in ["__AND__", "__OR__", "__NOT__"]: subrules = colander.SchemaNode(colander.Tuple(), name="rules") for rule in ruleset["rules"]: subrules.add(build_rule_schema(rule, check_matrix)) schema.add(subrules) else: op_choices = check_matrix[ruleset["field"]]["ops"] cast_to = check_matrix[ruleset["field"]]["type"] schema.add( colander.SchemaNode( colander.String(), validator=colander.OneOf(op_choices), name="op" ) ) schema.add( colander.SchemaNode( colander.String(), name="value", validator=RuleFieldType(cast_to) ) ) return schema class ConfigTypeSchema(colander.MappingSchema): type = colander.SchemaNode(colander.String(), missing=None) config = colander.SchemaNode(UnknownType(), missing=None) class MappingListSchema(colander.SequenceSchema): config = colander.SchemaNode(UnknownType())