Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Events API: Difference between revisions

From MoodleDocs
No edit summary
No edit summary
Line 3: Line 3:
|name = Events 2
|name = Events 2
|state = In early specification
|state = In early specification
|tracker =  
|tracker = MDL-39797
|discussion =  
|discussion =  
|assignee = Backend Team
|assignee = Backend Team

Revision as of 04:29, 23 May 2013

Note: This page is a work-in-progress. Feedback and suggested improvements are welcome. Please join the discussion on moodle.org or use the page comments.

Events 2
Project state In early specification
Tracker issue MDL-39797
Discussion
Assignee Backend Team


What are events?

Events are atomic pieces of information relating to an action in the system. Such actions are primarily the result of user actions, but could also be the result of the cron process or administration actions undertaken via the command line.

When an action takes place, an event is created by a core API or plugin. The Events system then disseminates this event information to handlers registered as observing events. In this way, the events system acts as a communication backbone throughout the Moodle system.

Why is a new events system needed?

The need to improve the Events system was prompted by a need for a richer and more efficient logging system, however the benefits of this improvement will be useful to other parts of Moodle that observe event information.

  • The first step is to make Events more specific.
  • It will be possible to subscribe to '*' event, which means to potentially observe, and selectively deal with, all events.
  • The logging system will become an event handler, observing events and directing them to logging storage plugins in a controllable way.

Performance

This proposal will not destroy Moodle performance because ...

We will test this by ...

(Please complete this section.)

Events API

Each plugin will define the events that it can report (trigger) by overloading an abstract base class, once for each possible event. This approach will have several benefits.

Events will be active objects
When they are triggered and possibly after they are reinstantiated (say, when they are retrieved from a log), an event object will be able to provide callback functions for various purposes (such as capability checks).
Automatic inclusion
Event class definitions will be automatically included when needed, without having to maintain lists of known event types. New event definitions can be added without the need to upgrade.
Maintainability
It will be easy to add new events and modify existing events. Plugin authors will be able to report minimal event information or richer information as needed.
Self documenting
The behaviour of events will be combined with the definition of events in one place (file). It will be easy for event handler writers to know what events a plugin can trigger.
Quick, self-validating data structure
As events are instantiated objects, the PHP processor will validate the structure and type of event classes. This does not ensure data value validity, but does give some assurance of consistency.

Triggering events

  • All event descriptions are objects extending event_base (which is defined in core)
  • Each event class name is a unique identifier of the event
  • Class names will follow the identifier scheme pluginfullname_event_something_happened. Core events will have prefix 'moodle'.
  • Plugins define each event class in separate file. File name must match class name, for example: plugindir/classes/event/something_happened.php
  • The event identifier suffix (something_happened in the example above) should be SUBJECT_VERB. There are recommendations for verbs to use in events names [1].

Examples: moodle_event_course_completed, mod_assign_event_submission_commented, mod_forum_event_post_shared, mod_forum_event_post_responded etc.

[TODO] We need to agree on "teacher verbs" to use when entity (course, module, etc.) was created, updated and/or deleted

  • Ideally, it should be possible to trigger an event without gathering additional information, just for the event. To reduce cost of data gathering, specifically the cost of database reads, at least the minimal values needed to trigger an event should be already available in variables.

Examples of triggering an event: mod_myplugin_event_something_happened::create_and_trigger(array('courseid' => XXX, 'userid' => YYY, 'data' => ZZZ)); or $event = mod_myplugin_event_something_happened::create(array('courseid' => XXX, 'userid' => YYY, 'data' => ZZZ)); // ... more code that may delete or modify the entities used when creating an event object $event->trigger();

Backward compatibility

  • moodle core and standard plugins will replace all usages of events_trigger() and add_to_log() with proper events
  • For events that already exist in Moodle 2.5 the additional legacy information should be added to the event data (in properties 'legacyeventname' and 'legacyeventdata')
  • For events that substitute add_to_log() calls the additional properties 'legacylogaction' and 'legacylogdata' will be specified.
  • Function events_trigger() will create and trigger an instance of event_legacy class with non-empty 'legacyeventname' property and optional 'legacyeventdata'
  • Function add_to_log() will create and trigger an instance of event_legacy class with non-empty 'legacylogaction' property

Handling (observing) events

  • Event handlers can be described as it is done now in plugindir/db/events.php, this file is parsed during install/upgrade of plugin and all handlers are removed on uninstall
  • It is possible to subscribe to all events (*)
  • If event handler refers to the old (2.5) name of event, it will be used only for events that have corresponding 'legacyeventname' property. If it refers to 2.6 event class name it will be used with full data
  • It is also possible to dynamically register/unregister handlers but they are not recommended (same as it is with dynamic caches definitions). Ideally they should only be used in unit and/or behat tests

Handlers (observers) sequence

  • Event handlers can also have an attribute 'sortorder' (positive or negative, default 0).
  • User with appropriate capability can overwrite the sequence of handlers for each event type

Event properties

Those are the fields that an event could define. Depending on the type of event, some can be optional and some can be mandatory. Not all of the mandatory fields have to be set when triggering the event as some logic can help defining them in the constructor. For example, we can use $USER to find out what user is currently logged in.

There are suggested list of properties, with some mandatory fields (*) defined for the base class.

  • Event specification version number* version
  • Event name* name (Automatically created using the class name)
  • Type* type
    • Error
    • User action
      • Procedural action
      • Manual action
    • System log
  • Datetime* time (milliseconds? DateTime object? Let's keep in mind that timestamps are unreliable as they don't include timezone!)
  • Context ID* contextid
  • Category ID coursecatid
  • Category name coursecatname
  • Course ID* courseid
  • Course name coursename
  • Course module ID* cmid
  • Course module name cmname
  • Component* component (Component declaring the event, not the one triggering it)
    • core/moodle
    • mod_assign
    • mod_workshop
    • qtype_match
  • Actor* actor (user, cli, cron, ...) (We cannot always identify what is the source of the action, so this would either be 'User' or 'System' and so the property userid is enough)
  • User ID* userid (User ID or 0 when it's CLI, Cron, or the System)
  • Real user ID* realuserid (When logged in as, store the real user ID)
  • User IP address* userip
  • Action* action (The exact action that is performed on the object)
    • created
    • moved (for a course, a section, a module, a user between groups)
    • submitted (for an assignment, or a message)
    • ended (for a submission phase for instance)
  • Object* object (Defines the object on which the action is performed)
    • user
    • section
    • assignment
    • submission_phase
  • Object ID* objectid
  • Object name objectname (Human readable identifier of the object: Mark Johnson, Course ABC, ...)
  • Object URL objecturl (URL to notice the changes)
  • Associated object associatedobject (Object associated to the subject. Ie: category of origin when a course is moved. User to whom a message is sent.)
    • section
    • category
    • user (to whom you sent a message)
  • Associated object ID associatedobjectid
  • Associated users IDs associatedusersids (see get_affected_users())
    • Typically used when the action has multiple users involved. For instance, bulk uploading of users, or marking a group assignment.
  • Transaction type* crud (CRUD)
    • create
    • read
    • update
    • delete
  • Level* level (Reserved for internal use, probably no more than 3 types, default to normal)
    • major
    • normal
    • minor
  • Severity severity (In the case of error logging, we would need to set the severity according to http://tools.ietf.org/html/rfc5424#section-6.2.1, defaults to info)
  • Data data (any extra field specific to this event)
  • Current URL currenturl

Data

The field data should be a placeholder for any extra information which is specific to the event. This array/object of values could describe in more details an entry which has been deleted by storing the data, or an entry updated by storing the differences, etc.... This data could ideally be converted to JSON (by a logger) to easily be stored, which means that PHP Objects cannot be included in there. We would not rely on serialized data as they have been proven not reliable.

Filtering events

Observers should be able to filter events based on information that they are interested in. Damyon pointed out that if you want to filter all the events that happen to a specific user, you would have problems when grading a group assignment, because there is no track of who was in that group. Though, we could store this information in $data, but this is not filterable without knowing what specific data each event is defining.

In order to be able to filter by users, we could add a method get_all_affected_users() which combines subject, object and associated object when relevant. But more importantly, this method can add extra heavy logic, specific to the event, to retrieve more users, for example the users in a group.

event_base class

abstract class event_base implements IteratorAggregate {

 // ... constants
 // ... all properties as protected variables, magic function __get() for all properties
 protected function __construct() {
 }
 public static function create($args = array()) {
   $event = new self();
   // ... Fill the properties from $args
   return $event;    
 }
 public static final function restore($object) {
   $event = new self();
   // .. restore each property from $object to $event
   return $event;
 }
 public static final function create_and_trigger($args = array()) {
   $event = self::create($args);
   $event->trigger();
 }
 public final function trigger() {
   // ... 
 }
 abstract public static function get_name();
 abstract public static function get_description();
 abstract public function can_view($user = null);
 abstract public function get_message();
 abstract public function get_all_affected_users();
 public function getIterator() {
   // ...
 }

}

Methods that must be overwritten

__construct() [protected]

Here plugins can initialise all properties that are static for this event (CRUD, object, subject). No dynamic values allowed here because the constructor can also be called when event is restored from log, so current time, user and course will be unapplicable.

create($args = array()) [static]

Main method that is used when plugin creates an event to trigger it. Plugins can overwrite with filling the default dynamic values such as $USER->id, $PAGE->get_course(), etc. Still no DB queries or expensive operations should be used here.

get_name() [static]

Static function returining human-readable localised event name. It will usually be called by reports displaying the logs or plugins that allow to configure logged event types. Recommended code:

return new lang_string('event_eventname', 'pluginname');

get_description() [static]

Static function returining human-readable localised event description. Recommended code:

return new lang_string('event_eventname_desc', 'pluginname');

can_view($user = null)

Allows to check if the user has capability to view this event (usually executed on event object restored from log).

get_message()

Method returning a human readable and localised string about what happened, using the event properties. Example: Fred deleted the blog post named 'Blah'.

get_all_affected_users()

Some events might affect multiple users (users in group, in course, bulk operation), but this information could be hard to compute. This method could be used by observers to retrieve additional information knowing that this can have a heavy cost! But this could also just return some information from the $data parameter, as only the event knows its structure.

Events naming convention

Clear event names help developers reading what events are triggered and defining the events properties when defining the event class.

Structure of existing events

Subject Action Object <relationship> Object 2 One to many
System changed completion of activity
System uploaded assessable_content
System done assessable_files Many
System uploaded assessable_file
System submitted assessable
System added blog_entry
System deleted blog_entry
System edited blog_entry
System added cohort
System deleted cohort
System added member to cohort
System removed member from cohort
System updated cohort
System deleted course_category
System completed course
System removed content of course
System created course
System deleted course
System restored course
System updated course
System created group
System deleted group
System created grouping
System deleted grouping
System deleted groupings Many
System removed groups from groupings Many
System updated grouping
System deleted groups Many
System updated group
System added member to group
System removed member from group
System removed members from group Many
System created module
System deleted module
System assigned role to user
System unassigned role to user
System started attempt of quiz
System created user
System deleted user
System enrolled user
System updated enrolment of user
System login
System logout
System unenrolled user
System updated user
System viewed workshop

Rules suggestion

<component>_event_<object2>_<object>_<verb>
  • component_event_user_created
  • component_event_activity_completion_changed
  • component_event_user_enrolment_created or user_enrolled
  • component_event_user_enrolment_removed or user_unenrolled
  • component_event_user_enrolment_modified
  • component_event_cohort_member_added
  • component_event_group_added
  • component_event_grouping_group_added
  • component_event_user_message_sent
  • component_event_users_imported

So it becomes readable like this:

<subject> <action> [a/the] <object> [in/of/from/to] <object2>
  • System created a user
  • System changed the completion of activity
  • System created an enrolment for user
  • System modified the enrolment of user
  • System added a member to cohort
  • System added group
  • System added group to grouping
  • System sent message to user
  • System imported users

Notes

  1. The subject of the sentence is implicit. It is System or User, so it should never been defined in the event (here I used System to be less confusing).
  2. We must not add course as a second object (object2) if it's implicit that the action is related to a course because we have the course information in the event properties.
  3. Plural must be used on objects when it's a One to Many relationship. Ex: bulk import, mass deletion, ... In any other case, use singular.

Example events

One to many

In 2.5, some events are triggered when an action happens on multiple objects. We have to decide whether we want to keep supporting One to Many events or not.

Accuracy

When uploading a bunch of users using the CSV upload feature, if only one event is triggered, it means that the observers of user_created won't be triggered. And so some functionalities can be lost as, as a plugin developer, I expect this user_created to be triggered regardless of the way they have been uploaded. Of course, the developer could observe the event bulk_user_imported, but that means that he could miss some relevant observers.

This applies to existing events.

Performance

Triggering one event is cheaper then repeating the same events x times...

Information tracking

A bulk event, might not be verbose enough to allow for proper logging afterwards. Though this is the responsability of the logger, we probably want to make it easy to store relevant information.

Double event

In the case of a bulk user import, if we were to trigger an event per user created, we probably want to trigger one event 'user_bulk_upload_started' when the action starts.

Logging

[TODO] Shift this to the logging document.

This section is deliberately not called "Logging API" because Moodle does not provide any logging API. All plugins that perform logging and analyse logs to display reports can do it as they wish. Events API defines how to subscribe to events and use them.

  • In places where we previously added information to 'the' log we must generate the new event.
  • The standard Moodle distribution will include simple DB logging plugin storing the data in new format. There will also be an section (optional) legacy logging plugin that will store data in the old format in {log} table. All standard reports will be upgraded to use the new logging system that will use a logging retrieval plugin to access log information. Custom report plugins will be able to query the {log} table, but should ideally shift to the new logging system over time, so that organisations are not forced to continue double logging.

From event to report

There are four steps of how event is converted into report:

  1. Handling of events and filtering what needs to be logged ("What to log")
  2. Storing the events data in the log storage (DB, filesystem, etc.) ("How to log")
  3. Retriving the data from log storage ("How to retrieve")
  4. Displaying the data in form of the report ("How to display")

One plugin can cover one or several steps. It is also possible to create a report that has built-in logging and listens to events (all 4 steps). Also external logging systems do not need to care about steps 3 and 4 at all.

Moodle standard distribution provides a suggestion on logging-report chain that can be followed by 3rd party plugins and may be not.

Logging plugins relation

Standard logging plugins

Standard Moodle 2.6 distribution will include 4 plugins:

Legacy logging plugin (tool_loglegacy)

Covers steps 1-3 from above. Listens to all events, for events that have non-empty 'legacylogaction' property adds the data to {log} table using the 'legacylogdata' object.

Core functions get_logs(), get_logs_usercourse(), get_logs_userday() will be checking if this plugin is installed and redirect to its appropriate functions.

All report plugins that use table {log} should use this plugin.

Event logging handling plugin (tool_eventobserver)

Responsible for step 1 from above. Has functions pluginname_register_log_instance() and pluginname_unregister_log_instance() that allow to add/remove log storage instance and also allows admin to configure what kind of events to store for each log instance.

Event logging DB storage plugin (tool_logdbstorage)

Responsible for step 2 from above. Has function pluginname_get_log_instances(). Allows admin to create storage instances and assign event handling plugin to fill each of them

Event logging driver plugin (tool_logdbdriver)

Responsible for step 3 from above. This plugin can only work with tool_logdbstorage. It provides functions to access data in log.

Development stages

Stage 1

  • Describe classes event_base and event_legacy
  • Current events triggering:
    • Rename function events_trigger() to events_trigger_old()
    • Create function events_trigger() that will create the instance of event_legacy with filled properties 'legacyeventname' and 'legacyeventdata'
    • In function event_base::trigger() check in case the event has property 'legacyeventname' execute events_trigger_old($this->legacyeventname, $this->legacyeventdata)
  • Current adding to logs:
    • Rename function add_to_log() to add_to_log_old()
    • Create function add_to_log() that will create the instance of event_legacy with filled properties 'legacylogname' and 'legacylogdata'
    • In function event_base::trigger() check in case the event has property 'legacylogname' execute add_to_log_old($this->legacylogname, ...)

After completing this stage everything should continue to work as it did before and we can start parallel work on further stages

Stage 2 (requires completion of Stage 1)

  • Create event classes and replace existing calls to events_trigger() and add_to_log() with proper event triggering (but still specifying properties 'legacy*')
  • Add more events throughtout the standard Moodle package

This is actually the most amount of work in this project

Stage 3 (requires completion of Stage 1)

  • Remove events_trigger_old() and re-write the existing event triggering system to have better performance and not query database so often
  • Allow handling of '*' (any) event

Stage 4 (requires completion of Stage 3)

  • Create plugin tool_loglegacy and move there all function querying / inserting into {log} table
  • The existing functions accessing table {log} (such as get_logs(), get_logs_usercourse(), etc.) should check if plugin is installed and call appropriate function from it
  • Make sure nobody else in core reads or writes from table {log}
  • Remove function add_to_log_old() and it's usage in event_base::trigger(), instead plugin should listen to '*' events and log events with non-empty legacylogname
  • If any of standard plugins work with {log} table they should define the dependency on this plugin

Stage 5 (requires completion of Stage 4)

  • Create 3 plugins that listen to events, store the new events objects in new log table, query the new log table and return data
  • Rewrite existing reports and other standard plugins to use the new events system, remove dependency from tool_loglegacy