blob: 6ddf474708cf34e4c042d1cb39a53657fa238776 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
from userale.version import __version__
from userale.format import JsonFormatter
from PyQt5.QtCore import QObject, QEvent, QTimer
from collections import Counter
import datetime, time
import logging
import uuid
import atexit
_ = JsonFormatter
class Ale (QObject):
def __init__(self,
output="userale.log",
user=None,
session=None,
toolname=None,
toolversion=None,
keylog=False,
interval=5000,
resolution=100,
shutoff=[]):
"""
:param output: [str] The file or url path to which logs will be sent
:param user: [str] Identifier for the user of the application
:param session: [str] Session tag to track same user with multiple sessions. If a session is not provided, one will be created
:param toolname: [str] The application name
:param toolversion: [str] The application version
:param keylog: [bool] Should detailed key logs be recorded. Default is False
:param interval: [int] The minimum time interval in ms between batch transmission of logs. Default is 5000ms.
:param resolution: [int] Delay in ms between instances of high frequency logs like mousemoves, scrolls, etc. Default is 100ms (10Hz).
:param shutoff: [list] Turn off logging for specific events
An example log will appear like this:
.. code-block:: python
{
'target': 'testLineEdit',
'path': ['Example', 'testLineEdit'],
'clientTime': '2016-08-03 16:12:03.460573',
'location': {'x': 82, 'y': 0},
'type': 'mousemove',
'userAction': 'true',
'details' : {},
'userId': 'userABC1234',
'session': '5ee42ccc-852c-44d9-a937-28d7901e4ead',
'toolName': 'myApplication',
'toolVersion': '3.5.0',
'useraleVersion': '0.1.0'
}
"""
QObject.__init__(self)
# UserAle Configuration
self.output = output
self.user = user
# Autogenerate session id if session is not configured
self.session = session if session is not None else str (uuid.uuid4 ())
self.toolname = toolname
self.toolversion = toolversion
self.keylog = keylog
self.interval = interval
self.resolution = resolution
self.shutoff = shutoff
# Configure logging
self.logger = logging
self.logger.basicConfig(level=logging.INFO,
filename=self.output,
format='%(message)s')
# Mapping of all events to methods
self.map = {
QEvent.MouseButtonPress: {'mousedown': self.handleMouseEvents},
QEvent.MouseButtonRelease: {'mouseup': self.handleMouseEvents},
QEvent.MouseMove: {'mousemove': self.handleMouseEvents},
QEvent.Enter: {'mouseenter': self.handleMouseEvents},
QEvent.Leave: {'mouseleave': self.handleMouseEvents},
QEvent.DragEnter: {'dragenter': self.handleDragEvents},
QEvent.DragLeave: {'dragleave': self.handleDragEvents},
QEvent.DragMove: {'dragmove': self.handleDragEvents},
QEvent.Drop: {'dragdrop': self.handleDragEvents},
QEvent.KeyPress: {'keypress': self.handleKeyEvents},
QEvent.KeyRelease: {'keyrelease': self.handleKeyEvents},
QEvent.Move: {'move': self.handleMoveEvents},
QEvent.Resize: {'resize': self.handleResizeEvents},
QEvent.Scroll: {'scroll': self.handleScrollEvents}
}
# Turn on/off keylogging & remove specific filters
for key in list (self.map):
name = list (self.map[key]) [0]
if name in self.shutoff or (not self.keylog and (name == 'keypress' or name == 'keyrelease')):
del self.map [key]
# Sample rate
self.hfreq = [QEvent.MouseMove, QEvent.DragMove, QEvent.Scroll]
# Sample Timer
self.timer = QTimer ()
self.timer.timeout.connect (self.aggregate)
self.timer.start (self.resolution)
# Cleanup Timer
# self.timer2 = QTimer ()
# self.timer2.timeout.connect (self.sample2)
# self.timer2.start (0)
#self.destroyed.connect (Ale._on_destroyed)
# Batch transmission of logs
self.intervalID = self.startTimer (self.interval)
# Temporary storage for logs
self.logs = []
self.hlogs = []
# Register Exit hanldler
atexit.register (self.cleanup)
def eventFilter (self, object, event):
'''
:param object: [QObject] The object being watched.
:param event: [QEvent] The event triggered by a user action.
:return: [bool] Propagate filter up if other objects needs to be handled
Filters events for the watched widget (in this case, QApplication)
'''
data = None
t = event.type ()
if t in self.map:
# Handle leaf node
if len(object.children ()) == 0:
# if object.isWidgetType () and len(object.children ()) == 0:
name = list (self.map [t].keys())[0]
method = list (self.map [t].values())[0]
data = method (name, event, object)
# Handle window object
else:
# How to handle events on windows? It comes before the child widgets in window?
# Either an event actually ocurred on window or is an effect of event propagation.
pass
# Filter data to higher or lower priority list
if data is not None:
if t in self.hfreq and t in self.map: # data is in watched list and is a high frequency log
temp = (name, event, object)
self.hlogs.append (temp)
else:
self.logs.append (data)
return super (Ale, self).eventFilter (object, event)
def cleanup (self):
'''
Clean up any dangling logs in self.logs or self.hlogs
'''
self.aggregate ()
self.dump ()
def timerEvent (self, event):
'''
:param object: [list] List of events
:return: [void] Emit events to file
Routinely dump data to file or send over the network
'''
self.dump ()
def dump (self):
if len(self.logs) > 0:
# print ("dumping {} logs".format (len (self.logs)))
self.logger.info (_(self.logs))
self.logs = [] # Reset logs
def aggregate (self):
'''
Sample high frequency logs at self.resolution. High frequency logs are consolidated down to a single log event
to be emitted later
'''
if len (self.hlogs) > 0:
# print ("agging {} logs".format (len (self.hlogs)))
agg_events = Counter (self.hlogs)
# Iterate over collapsed collection to generate a single log per event
# Location information is lost due to consolidation.
# @todo develop hashing funciton or new counter to generate avg x and avg y location
for event, counter in agg_events.items ():
aggdata = self.__create_msg (event[0], event[1], event[2], details={"count" : counter})
self.logs.append (aggdata)
self.hlogs = []
def getSender (self, object):
'''
:param object: [QObject] The object being watched.
:return: [QObject] The QObject
Fetch the QObject who triggered the event
'''
sender = None
try:
sender = object.sender () if object.sender() is not None else None
except:
pass
return sender
def getSelector (self, object):
"""
:param object: [QObject] The base class for all Qt objects.
:return: [str] The Qt object's name
Get target object's name (object defined by user or object's meta class name).
"""
return object.objectName () if object.objectName () else object.staticMetaObject.className ()
def getLocation (self, event):
"""
:param event: [QEvent] The base class for all event classes.
:return: [dict] A dictionary representation of the x and y positions of the mouse cursor.
Grab the x and y position of the mouse cursor, relative to the widget that received the event.
"""
try:
return {"x" : event.pos ().x (), "y" : event.pos ().y ()}
except:
return None
def getPath (self, object):
"""
:param object: [QObject] The base class for all Qt objects.
:return: [list] List of QObjects.
Generate the entire object hierachy from root to leaf node.
"""
if object.parent() is not None:
return self.getPath (object.parent()) + [self.getSelector (object)]
else:
return [self.getSelector (object)]
def getClientTime (self):
"""
:return: [str] String representation of the time the event was captured.
Capture the time the event was captured in milliseconds since the UNIX epoch (January 1, 1970 00:00:00 UTC)
"""
return int (time.time() * 1000)
def handleMouseEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a mouse event.
Returns the userale log representing all mouse event data.
"""
details = {}
return self.__create_msg (event_type, event, object, details=details)
def handleKeyEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a key event.
Returns the userale log representing all key events, including key name and key code.
"""
details = {"key" : event.text (), "keycode" : event.key ()}
return self.__create_msg (event_type, event, object, details=details)
def handleDragEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a drag event.
Returns the userale log representing all drag events.
"""
details = {}
try:
details ["source"] = self.getSelector (event.source ())
except:
details ["source"] = None
return self.__create_msg (event_type, event, object, details=details)
def handleMoveEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a drag event.
Returns the userale log representing all move events.
"""
details = {"oldPos" : {"x" : event.oldPos ().x (), "y" : event.oldPos ().y ()}}
return self.__create_msg (event_type, event, object, details=details)
def handleResizeEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a resize event.
Returns the userale log representing all resize events.
"""
details = {"size" : {"height" : event.size ().height (), "width" : event.size ().width ()},
"oldSize": {"height" : event.oldSize ().height (), "width" : event.oldSize ().width ()}}
return self.__create_msg (event_type, event, object, details=details)
def handleScrollEvents (self, event_type, event, object):
"""
:param event_type: [str] The string representation of the type of event being triggered by the user.
:param event: [QEvent] The base class for all event classes.
:param object: [QObject] The base class for all Qt objects.
:return: [dict] A userale log describing a scroll event.
Returns the userale log representing all scroll events.
"""
return self.__create_msg (event_type, event, object)
def __create_msg (self, event_type, event, object, details={}):
"""
Geneate UserAle log describing an event.
"""
data = {
"target": self.getSelector (object),
"path": self.getPath (object),
"clientTime": self.getClientTime (),
"location": self.getLocation (event),
"type": event_type ,
"userAction": True, # legacy field
"details" : details,
"userId": self.user,
"session": self.session,
"toolName": self.toolname,
"toolVersion": self.toolversion,
"useraleVersion": __version__
}
return data