Add AtomPub handler

Adds AtomPub handler to send events to an atom server as a feed.

Also, fixed Usage driver to save new events directly from pipeline.
Fixed minor (but annoyingly hard to diagnose) bug in db layer
    where it didn't recognize 'long' values.

Change-Id: I792e9f77accfea4583fd75805a9ff0d946827df8
This commit is contained in:
Monsyne Dragon 2015-08-19 06:46:29 +00:00
parent 665c9ad328
commit b5191bb105
7 changed files with 519 additions and 47 deletions

View File

@ -1,7 +1,7 @@
[metadata] [metadata]
description-file = README.md description-file = README.md
name = winchester name = winchester
version = 0.57 version = 0.62
author = Monsyne Dragon author = Monsyne Dragon
author_email = mdragon@rackspace.com author_email = mdragon@rackspace.com
summary = An OpenStack notification event processing library. summary = An OpenStack notification event processing library.

151
tests/test_atompub.py Normal file
View File

@ -0,0 +1,151 @@
# Copyright (c) 2015 Rackspace
#
# 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 unittest2 as unittest
import mock
from winchester import pipeline_handler
class TestException(Exception):
pass
class TestAtomPubHandler(unittest.TestCase):
def test_constructor_event_types(self):
fakeurl = 'fake://'
h = pipeline_handler.AtomPubHandler(fakeurl)
self.assertEqual(h.included_types, ['*'])
self.assertEqual(h.excluded_types, [])
h = pipeline_handler.AtomPubHandler(fakeurl,
event_types='test.thing')
self.assertEqual(h.included_types, ['test.thing'])
self.assertEqual(h.excluded_types, [])
h = pipeline_handler.AtomPubHandler(fakeurl,
event_types=['test.thing'])
self.assertEqual(h.included_types, ['test.thing'])
self.assertEqual(h.excluded_types, [])
h = pipeline_handler.AtomPubHandler(fakeurl,
event_types=['!test.thing'])
self.assertEqual(h.included_types, ['*'])
self.assertEqual(h.excluded_types, ['test.thing'])
def test_match_type(self):
event_types = ["test.foo.bar", "!test.wakka.wakka"]
h = pipeline_handler.AtomPubHandler('fakeurl',
event_types=event_types)
self.assertTrue(h.match_type('test.foo.bar'))
self.assertFalse(h.match_type('test.wakka.wakka'))
self.assertFalse(h.match_type('test.foo.baz'))
event_types = ["test.foo.*", "!test.wakka.*"]
h = pipeline_handler.AtomPubHandler('fakeurl',
event_types=event_types)
self.assertTrue(h.match_type('test.foo.bar'))
self.assertTrue(h.match_type('test.foo.baz'))
self.assertFalse(h.match_type('test.wakka.wakka'))
def test_handle_events(self):
event_types = ["test.foo.*", "!test.wakka.*"]
h = pipeline_handler.AtomPubHandler('fakeurl',
event_types=event_types)
event1 = dict(event_type="test.foo.zazz")
event2 = dict(event_type="test.wakka.zazz")
event3 = dict(event_type="test.boingy")
events = [event1, event2, event3]
res = h.handle_events(events, dict())
self.assertEqual(events, res)
self.assertIn(event1, h.events)
self.assertNotIn(event2, h.events)
self.assertNotIn(event3, h.events)
def test_format_cuf_xml(self):
expected = ('<event xmlns="http://docs.rackspace.com/core/event" '
'xmlns:nova="http://docs.rackspace.com/event/nova" '
'version="1" id="1234-56789" resourceId="98765-4321" '
'resourceName="" dataCenter="TST1" region="TST" '
'tenantId="" startTime="2015-08-10T00:00:00Z" '
'endTime="2015-08-11T00:00:00Z" type="USAGE">'
'<nova:product version="1" '
'serviceCode="CloudServersOpenStack" '
'resourceType="SERVER" flavorId="" flavorName="" '
'status="ACTIVE" osLicenseType="WINDOWS" '
'bandwidthIn="" bandwidthOut=""/></event>')
d1 = datetime.datetime(2015, 8, 10, 0, 0, 0)
d2 = datetime.datetime(2015, 8, 11, 0, 0, 0)
d3 = datetime.datetime(2015, 8, 9, 15, 21, 0)
event = dict(message_id='1234-56789',
event_type='test.thing',
audit_period_beginning=d1,
audit_period_ending=d2,
launched_at=d3,
instance_id='98765-4321',
state='active',
state_description='',
rax_options='4')
extra = dict(data_center='TST1', region='TST')
h = pipeline_handler.AtomPubHandler('fakeurl',
extra_info=extra)
res, content_type = h.format_cuf_xml(event)
self.assertEqual(res, expected)
self.assertEqual(content_type, 'application/xml')
def test_generate_atom(self):
expected = ("""<atom:entry xmlns:atom="http://www.w3.org/2005/Atom">"""
"""<atom:id>urn:uuid:12-34</atom:id>"""
"""<atom:category term="test.thing.bar" />"""
"""<atom:category """
"""term="original_message_id:56-78" />"""
"""<atom:title type="text">Server</atom:title>"""
"""<atom:content type="test/thing">TEST_CONTENT"""
"""</atom:content></atom:entry>""")
event = dict(message_id='12-34',
original_message_id='56-78',
event_type='test.thing')
event_type = 'test.thing.bar'
ctype = 'test/thing'
content = 'TEST_CONTENT'
h = pipeline_handler.AtomPubHandler('fakeurl')
atom = h.generate_atom(event, event_type, content, ctype)
self.assertEqual(atom, expected)
@mock.patch.object(pipeline_handler.requests, 'post')
@mock.patch.object(pipeline_handler.AtomPubHandler, '_get_auth')
def test_send_event(self, auth, rpost):
test_headers = {'Content-Type': 'application/atom+xml',
'X-Auth-Token': 'testtoken'}
auth.return_value = test_headers
test_response = mock.MagicMock('http response')
test_response.status_code = 200
rpost.return_value = test_response
h = pipeline_handler.AtomPubHandler('fakeurl', http_timeout=123,
wait_interval=10, max_wait=100)
test_atom = mock.MagicMock('atom content')
status = h._send_event(test_atom)
self.assertEqual(1, auth.call_count)
self.assertEqual(1, rpost.call_count)
rpost.assert_called_with('fakeurl',
data=test_atom,
headers=test_headers,
timeout=123)
self.assertEqual(status, 200)

View File

@ -211,7 +211,7 @@ class TestUsageHandler(unittest.TestCase):
f['event_type']) f['event_type'])
self.assertEqual("now", f['timestamp']) self.assertEqual("now", f['timestamp'])
self.assertEqual(123, f['stream_id']) self.assertEqual(123, f['stream_id'])
self.assertEqual("inst", f['payload']['instance_id']) self.assertEqual("inst", f['instance_id'])
self.assertEqual("None", f['error']) self.assertEqual("None", f['error'])
self.assertIsNone(f['error_code']) self.assertIsNone(f['error_code'])
@ -228,7 +228,7 @@ class TestUsageHandler(unittest.TestCase):
f['event_type']) f['event_type'])
self.assertEqual("now", f['timestamp']) self.assertEqual("now", f['timestamp'])
self.assertEqual(123, f['stream_id']) self.assertEqual(123, f['stream_id'])
self.assertEqual("inst", f['payload']['instance_id']) self.assertEqual("inst", f['instance_id'])
self.assertEqual("Error", f['error']) self.assertEqual("Error", f['error'])
self.assertEqual("UX", f['error_code']) self.assertEqual("UX", f['error_code'])
@ -301,11 +301,9 @@ class TestUsageHandler(unittest.TestCase):
env = {'stream_id': 123} env = {'stream_id': 123}
raw = [{'event_type': 'foo'}] raw = [{'event_type': 'foo'}]
events = self.handler.handle_events(raw, env) events = self.handler.handle_events(raw, env)
self.assertEqual(1, len(events)) self.assertEqual(2, len(events))
notifications = env['usage_notifications']
self.assertEqual(1, len(notifications))
self.assertEqual("compute.instance.exists.failed", self.assertEqual("compute.instance.exists.failed",
notifications[0]['event_type']) events[-1]['event_type'])
@mock.patch.object(pipeline_handler.UsageHandler, '_process_block') @mock.patch.object(pipeline_handler.UsageHandler, '_process_block')
def test_handle_events_exists(self, pb): def test_handle_events_exists(self, pb):
@ -325,9 +323,7 @@ class TestUsageHandler(unittest.TestCase):
{'event_type': 'foo'}, {'event_type': 'foo'},
] ]
events = self.handler.handle_events(raw, env) events = self.handler.handle_events(raw, env)
self.assertEqual(3, len(events)) self.assertEqual(4, len(events))
notifications = env['usage_notifications']
self.assertEqual(1, len(notifications))
self.assertEqual("compute.instance.exists.failed", self.assertEqual("compute.instance.exists.failed",
notifications[0]['event_type']) events[-1]['event_type'])
self.assertTrue(pb.called) self.assertTrue(pb.called)

View File

@ -143,7 +143,12 @@ class DBInterface(object):
event_type = self.get_event_type(event_type, session=session) event_type = self.get_event_type(event_type, session=session)
e = models.Event(message_id, event_type, generated) e = models.Event(message_id, event_type, generated)
for name in traits: for name in traits:
e[name] = traits[name] try:
e[name] = traits[name]
except models.InvalidTraitType:
logger.error("Invalid trait for %s "
"(%s) %s" % (name, traits[name],
type(traits[name])))
session.add(e) session.add(e)
@sessioned @sessioned

View File

@ -160,6 +160,7 @@ class PolymorphicVerticalProperty(object):
ATTRIBUTE_MAP = {Datatype.none: None} ATTRIBUTE_MAP = {Datatype.none: None}
PY_TYPE_MAP = {unicode: Datatype.string, PY_TYPE_MAP = {unicode: Datatype.string,
int: Datatype.int, int: Datatype.int,
long: Datatype.int,
float: Datatype.float, float: Datatype.float,
datetime: Datatype.datetime, datetime: Datatype.datetime,
DBTimeRange: Datatype.timerange} DBTimeRange: Datatype.timerange}

View File

@ -15,12 +15,17 @@
# limitations under the License. # limitations under the License.
import abc import abc
import collections
import datetime import datetime
import fnmatch
import json
import logging import logging
import six import six
import time
import uuid import uuid
from notabene import kombu_driver as driver from notabene import kombu_driver as driver
import requests
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -358,33 +363,54 @@ class UsageHandler(PipelineHandlerBase):
self._confirm_delete(exists, deleted, delete_fields) self._confirm_delete(exists, deleted, delete_fields)
def _base_notification(self, exists): def _base_notification(self, exists):
apb, ape = self._get_audit_period(exists) basen = exists.copy()
return { if 'bandwidth_in' not in basen:
'payload': { basen['bandwidth_in'] = 0
'audit_period_beginning': str(apb), if 'bandwidth_out' not in basen:
'audit_period_ending': str(ape), basen['bandwidth_out'] = 0
'launched_at': str(exists.get('launched_at', '')), if 'rax_options' not in basen:
'deleted_at': str(exists.get('deleted_at', '')), basen['rax_options'] = '0'
'instance_id': exists.get('instance_id', ''), basen['original_message_id'] = exists.get('message_id', '')
'tenant_id': exists.get('tenant_id', ''), return basen
'display_name': exists.get('display_name', ''), # apb, ape = self._get_audit_period(exists)
'instance_type': exists.get('instance_flavor', ''), # return {
'instance_flavor_id': exists.get('instance_flavor_id', ''), # 'payload': {
'state': exists.get('state', ''), # 'audit_period_beginning': str(apb),
'state_description': exists.get('state_description', ''), # 'audit_period_ending': str(ape),
'bandwidth': {'public': { # 'launched_at': str(exists.get('launched_at', '')),
'bw_in': exists.get('bandwidth_in', 0), # 'deleted_at': str(exists.get('deleted_at', '')),
'bw_out': exists.get('bandwidth_out', 0)}}, # 'instance_id': exists.get('instance_id', ''),
'image_meta': { # 'tenant_id': exists.get('tenant_id', ''),
'org.openstack__1__architecture': exists.get( # 'display_name': exists.get('display_name', ''),
'os_architecture', ''), # 'instance_type': exists.get('instance_flavor', ''),
'org.openstack__1__os_version': exists.get('os_version', # 'instance_flavor_id': exists.get('instance_flavor_id', ''),
''), # 'state': exists.get('state', ''),
'org.openstack__1__os_distro': exists.get('os_distro', ''), # 'state_description': exists.get('state_description', ''),
'org.rackspace__1__options': exists.get('rax_options', '0') # 'bandwidth': {'public': {
} # 'bw_in': exists.get('bandwidth_in', 0),
}, # 'bw_out': exists.get('bandwidth_out', 0)}},
'original_message_id': exists.get('message_id', '')} # 'image_meta': {
# 'org.openstack__1__architecture': exists.get(
# 'os_architecture', ''),
# 'org.openstack__1__os_version': exists.get('os_version',
# ''),
# 'org.openstack__1__os_distro': exists.get('os_distro', ''),
# 'org.rackspace__1__options': exists.get('rax_options', '0')
# }
# },
# 'original_message_id': exists.get('message_id', '')}
def _generate_new_id(self, original_message_id, event_type):
# Generate message_id for new events deterministically from
# the original message_id and event type using uuid5 algo.
# This will allow any dups to be caught by message_id. (mdragon)
if original_message_id:
oid = uuid.UUID(original_message_id)
return uuid.uuid5(oid, event_type)
else:
logger.error("Generating %s, but origional message missing"
" origional_message_id." % event_type)
return uuid.uuid4()
def _process_block(self, block, exists): def _process_block(self, block, exists):
error = None error = None
@ -422,12 +448,13 @@ class UsageHandler(PipelineHandlerBase):
datetime.datetime.utcnow()), datetime.datetime.utcnow()),
'stream_id': int(self.stream_id), 'stream_id': int(self.stream_id),
'instance_id': instance_id, 'instance_id': instance_id,
'warnings': self.warnings} 'warnings': ', '.join(self.warnings)}
events.append(warning_event) events.append(warning_event)
new_event = self._base_notification(exists) new_event = self._base_notification(exists)
new_event['message_id'] = self._generate_new_id(
new_event['original_message_id'], event_type)
new_event.update({'event_type': event_type, new_event.update({'event_type': event_type,
'message_id': str(uuid.uuid4()),
'publisher_id': 'stv3', 'publisher_id': 'stv3',
'timestamp': exists.get('timestamp', 'timestamp': exists.get('timestamp',
datetime.datetime.utcnow()), datetime.datetime.utcnow()),
@ -466,11 +493,299 @@ class UsageHandler(PipelineHandlerBase):
} }
new_events.append(new_event) new_events.append(new_event)
env['usage_notifications'] = new_events return events + new_events
return events
def commit(self): def commit(self):
pass pass
def rollback(self): def rollback(self):
pass pass
class AtomPubException(Exception):
pass
cuf_template = ("""<event xmlns="http://docs.rackspace.com/core/event" """
"""xmlns:nova="http://docs.rackspace.com/event/nova" """
"""version="1" """
"""id="%(message_id)s" resourceId="%(instance_id)s" """
"""resourceName="%(display_name)s" """
"""dataCenter="%(data_center)s" """
"""region="%(region)s" tenantId="%(tenant_id)s" """
"""startTime="%(start_time)s" endTime="%(end_time)s" """
"""type="USAGE"><nova:product version="1" """
"""serviceCode="CloudServersOpenStack" """
"""resourceType="SERVER" """
"""flavorId="%(instance_flavor_id)s" """
"""flavorName="%(instance_flavor)s" """
"""status="%(status)s" %(options)s """
"""bandwidthIn="%(bandwidth_in)s" """
"""bandwidthOut="%(bandwidth_out)s"/></event>""")
class AtomPubHandler(PipelineHandlerBase):
auth_token_cache = None
def __init__(self, url, event_types=None, extra_info=None,
auth_user='', auth_key='', auth_server='',
wait_interval=30, max_wait=600, http_timeout=120, **kw):
super(AtomPubHandler, self).__init__(**kw)
self.events = []
self.included_types = []
self.excluded_types = []
self.url = url
self.auth_user = auth_user
self.auth_key = auth_key
self.auth_server = auth_server
self.wait_interval = wait_interval
self.max_wait = max_wait
self.http_timeout = http_timeout
if extra_info:
self.extra_info = extra_info
else:
self.extra_info = {}
if event_types:
if isinstance(event_types, six.string_types):
event_types = [event_types]
for t in event_types:
if t.startswith('!'):
self.excluded_types.append(t[1:])
else:
self.included_types.append(t)
else:
self.included_types.append('*')
if self.excluded_types and not self.included_types:
self.included_types.append('*')
def _included_type(self, event_type):
return any(fnmatch.fnmatch(event_type, t) for t in self.included_types)
def _excluded_type(self, event_type):
return any(fnmatch.fnmatch(event_type, t) for t in self.excluded_types)
def match_type(self, event_type):
return (self._included_type(event_type)
and not self._excluded_type(event_type))
def handle_events(self, events, env):
for event in events:
event_type = event['event_type']
if self.match_type(event_type):
self.events.append(event)
logger.debug("Matched %s events." % len(self.events))
return events
def commit(self):
for event in self.events:
event_type = event.get('event_type', '')
message_id = event.get('message_id', '')
try:
status = self.publish_event(event)
logger.debug("Sent %s event %s. Status %s" % (event_type,
message_id,
status))
except Exception:
original_message_id = event.get('original_message_id', '')
logger.exception("Error publishing %s event %s "
"(original id: %s)!" % (event_type,
message_id,
original_message_id))
def publish_event(self, event):
content, content_type = self.format_cuf_xml(event)
event_type = self.event_type_cuf_xml(event.get('event_type'))
atom = self.generate_atom(event, event_type, content, content_type)
logger.debug("Publishing event: %s" % atom)
return self._send_event(atom)
def generate_atom(self, event, event_type, content, content_type):
template = ("""<atom:entry xmlns:atom="http://www.w3.org/2005/Atom">"""
"""<atom:id>urn:uuid:%(message_id)s</atom:id>"""
"""<atom:category term="%(event_type)s" />"""
"""<atom:category """
"""term="original_message_id:%(original_message_id)s" />"""
"""<atom:title type="text">Server</atom:title>"""
"""<atom:content type="%(content_type)s">%(content)s"""
"""</atom:content></atom:entry>""")
info = dict(message_id=event.get('message_id'),
original_message_id=event.get('original_message_id'),
event=event,
event_type=event_type,
content=content,
content_type=content_type)
return template % info
def event_type_cuf_xml(self, event_type):
return event_type + ".cuf"
def format_cuf_xml(self, event):
tvals = collections.defaultdict(lambda: '')
tvals.update(event)
tvals.update(self.extra_info)
start_time, end_time = self._get_times(event)
tvals['start_time'] = self._format_time(start_time)
tvals['end_time'] = self._format_time(end_time)
tvals['status'] = self._get_status(event)
tvals['options'] = self._get_options(event)
c = cuf_template % tvals
return (c, 'application/xml')
def _get_options(self, event):
opt = int(event.get('rax_options', 0))
flags = [bool(opt & (2**i)) for i in range(8)]
os = 'LINUX'
app = None
if flags[0]:
os = 'RHEL'
if flags[2]:
os = 'WINDOWS'
if flags[6]:
os = 'VYATTA'
if flags[3]:
app = 'MSSQL'
if flags[5]:
app = 'MSSQL_WEB'
if app is None:
return 'osLicenseType="%s"' % os
else:
return 'osLicenseType="%s" applicationLicense="%s"' % (os, app)
def _get_status(self, event):
state = event.get('state')
state_description = event.get('state_description')
status = 'UNKNOWN'
status_map = {
"building": 'BUILD',
"stopped": 'SHUTOFF',
"paused": 'PAUSED',
"suspended": 'SUSPENDED',
"rescued": 'RESCUE',
"error": 'ERROR',
"deleted": 'DELETED',
"soft-delete": 'SOFT_DELETED',
"shelved": 'SHELVED',
"shelved_offloaded": 'SHELVED_OFFLOADED',
}
if state in status_map:
status = status_map[state]
if state == 'resized':
if state_description == 'resize_reverting':
status = 'REVERT_RESIZE'
else:
status = 'VERIFY_RESIZE'
if state == 'active':
active_map = {
"rebooting": 'REBOOT',
"rebooting_hard": 'HARD_REBOOT',
"updating_password": 'PASSWORD',
"rebuilding": 'REBUILD',
"rebuild_block_device_mapping": 'REBUILD',
"rebuild_spawning": 'REBUILD',
"migrating": 'MIGRATING',
"resize_prep": 'RESIZE',
"resize_migrating": 'RESIZE',
"resize_migrated": 'RESIZE',
"resize_finish": 'RESIZE',
}
status = active_map.get(state_description, 'ACTIVE')
if status == 'UNKNOWN':
logger.error("Unknown status for event %s: state %s (%s)" % (
event.get('message_id'), state, state_description))
return status
def _get_times(self, event):
audit_period_beginning = event.get('audit_period_beginning')
audit_period_ending = event.get('audit_period_ending')
launched_at = event.get('launched_at')
terminated_at = event.get('terminated_at')
if not terminated_at:
terminated_at = event.get('deleted_at')
start_time = max(launched_at, audit_period_beginning)
if not terminated_at:
end_time = audit_period_ending
else:
end_time = min(terminated_at, audit_period_ending)
if start_time > end_time:
start_time = audit_period_beginning
return (start_time, end_time)
def _format_time(self, dt):
time_format = "%Y-%m-%dT%H:%M:%SZ"
if dt:
return datetime.datetime.strftime(dt, time_format)
else:
return ''
def _get_auth(self, force=False, headers=None):
if headers is None:
headers = {}
if force or not AtomPubHandler.auth_token_cache:
auth_body = {"auth": {
"RAX-KSKEY:apiKeyCredentials": {
"username": self.auth_user,
"apiKey": self.auth_key,
}}}
auth_headers = {"User-Agent": "Winchester",
"Accept": "application/json",
"Content-Type": "application/json"}
logger.debug("Contacting auth server %s" % self.auth_server)
res = requests.post(self.auth_server,
data=json.dumps(auth_body),
headers=auth_headers)
res.raise_for_status()
token = res.json()["access"]["token"]["id"]
logger.debug("Token received: %s" % token)
AtomPubHandler.auth_token_cache = token
headers["X-Auth-Token"] = AtomPubHandler.auth_token_cache
return headers
def _send_event(self, atom):
headers = {"Content-Type": "application/atom+xml"}
headers = self._get_auth(headers=headers)
attempts = 0
status = 0
while True:
try:
res = requests.post(self.url,
data=atom,
headers=headers,
timeout=self.http_timeout)
status = res.status_code
if status >= 200 and status < 300:
break
if status == 401:
logger.info("Auth expired, reauthorizing...")
headers = self._get_auth(headers=headers, force=True)
continue
if status == 409:
# they already have this. No need to retry. (mdragon)
logger.debug("Duplicate message: \n%s" % atom)
break
if status == 400:
# AtomPub server won't accept content.
logger.error("Invalid Content: Server rejected content: "
"\n%s" % atom)
break
except requests.exceptions.ConnectionError:
logger.exception("Connection error talking to %s" % self.url)
except requests.exceptions.Timeout:
logger.exception("HTTP timeout talking to %s" % self.url)
except requests.exceptions.HTTPError:
logger.exception("HTTP protocol error talking to "
"%s" % self.url)
except requests.exceptions.RequestException:
logger.exception("Unknown exeption talking to %s" % self.url)
# If we got here, something went wrong
attempts += 1
wait = min(attempts * self.wait_interval, self.max_wait)
logger.error("Message delivery failed, going to sleep, will "
"try again in %s seconds" % str(wait))
time.sleep(wait)
return status
def rollback(self):
pass

View File

@ -193,14 +193,18 @@ class TriggerManager(object):
return self.time_sync.current_time() return self.time_sync.current_time()
def save_event(self, event): def save_event(self, event):
traits = event.copy() traits = {}
try: try:
message_id = traits.pop('message_id') message_id = event['message_id']
timestamp = traits.pop('timestamp') timestamp = event['timestamp']
event_type = traits.pop('event_type') event_type = event['event_type']
except KeyError as e: except KeyError as e:
logger.warning("Received invalid event: %s" % e) logger.warning("Received invalid event: %s" % e)
return False return False
for key, val in event.items():
if key not in ('message_id', 'timestamp', 'event_type'):
if val is not None:
traits[key] = val
try: try:
self.db.create_event(message_id, event_type, self.db.create_event(message_id, event_type,
timestamp, traits) timestamp, traits)