blob: b196e4bcde81b957499a994415b5970d56d4017c [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
import random
_ = JsonFormatter
class Ale (QObject):
"""
ALE Library
"""
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). Entering 0 disables it.
: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.getLogger ('userale')
self.logger.propagate = False
self.logger.setLevel (logging.INFO)
handler = logging.FileHandler (self.output)
self.logger.addHandler (handler)
# 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
if self.resolution > 0:
self.timer = QTimer ()
self.timer.timeout.connect (self.aggregate)
self.timer.start (self.resolution)
# 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.
'''
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:
print (_(data))
if self.resolution > 0 and t in self.hfreq and t in self.map: # data is in watched list and is a high frequency log
self.hlogs.append (data)
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
'''
if self.resolution > 0:
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):
'''
Write log data to file
'''
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)))
# Given target, path, location [median], return aggregate
# 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})
# aggdata = {"target": event[0], "type" : event[1], "details": {"count": counter}}
# print (aggdata)
# self.logs.append (aggdata)
self.logs.append (random.choice (self.hlogs))
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). If object is null, return "Undefined".
"""
try:
return object.objectName () if object.objectName () else object.staticMetaObject.className ()
except:
return "Undefined"
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.
"""
try:
if object.parent() is not None:
return self.getPath (object.parent()) + [self.getSelector (object)]
else:
return [self.getSelector (object)]
except:
return "Undefined"
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