blob: e82a916760afab82dffda79114f36c6b05d40097 [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
import 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] Enable logging of keystrokes.
: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 events need 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))
# data is in watched list and is a high frequency log
if self.resolution > 0 and t in self.hfreq and t in self.map:
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:
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. 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 dict 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] 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 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 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 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 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 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 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