Defining what happens when matching events are found

In a monitor, when the correlator detects a matching event, it triggers the action defined by the listener for that event. This section discusses what you can specify in the triggered actions.

Using variables

EPL supports the use of variables in monitors. Depending on where in the monitor you declare a variable, that variable is global or local:

  • Global. Variables declared in monitors and not inside actions or events are global variables. Global variables are in monitor scope.
  • Local. Variables declared inside actions are local variables. Local variables are in action scope.

A variable can be of any of the primitive or reference types that are listed under Types in the EPL Reference.

Information about variables is presented in the topics below.

See also Using action type variables.

Using global variables

Variables in monitor scope are global variables; you can access a global variable throughout the monitor. You can define global variables anywhere inside a monitor except in actions and event definitions. For example:

monitor SimpleShareSearch {
   // A monitor scope variable to store the stock received:
   //
   StockTick newTick;

This declares a global variable, newTick, that can be used anywhere within the SimpleShareSearch monitor including within any of its actions.

The order does not matter. In the following example, f is a global variable:

monitor Test {
   action onload() {
      print getZ().toString();
   }
   action getZ() returns integer {
      return f.z;
   }
   Foo f;
   event Foo{
      integer z;
   }
}

If you do not explicitly initialize the value of a global variable, the correlator automatically assigns a value to that global variable. See also Default values for types.

Using local variables

A variable that you declare inside an action is a local variable. You must declare a local variable (specifying its type) and initialize that variable before you can use it.

Although the correlator automatically initializes global variables that were not explicitly assigned a value, the correlator does not do this for local variables. For local variables, you must explicitly assign a value before you can use the variable.

If you try to inject an EPL file that declares a local variable and you have not initialized the value of that local variable before you try to use it, the correlator terminates injection of that file and generates a message such as the following: Local variable 'var2' might not have been initialized. EPL requires explicit assignment of values to local variables as a way of achieving the best performance.

When you declare a variable in an action, you can use that variable only in that action. You can declare a variable anywhere in an action, but you can use it only after you declare it and initialize it.

For example,

action anAction(integer a) returns integer {
   integer i;
   integer j;
   i := 10;
   j := a;
   return j + i;
}

You can use the local action variables, i and j in the action, anAction(), after you initialize them. The following generates an error:

action anAction2(integer a) returns integer {
   i := 10; // error, reference to undeclared variable i
   j := a; // error, reference to undeclared variable j
   integer i;
   integer j;
   i := 2;
   j := 5;
   return j + i;
}

Suppose that an action scope variable has the same name as a monitor scope variable. Within that action, after declaration of the action scope variable, any references to the variable resolve to the action scope variable. In other words, a local action variable always hides a global variable of the same name.

Consider again the definition for anAction2() in the previous code fragment, but with i and j variables declared in the monitor scope. The first use of i and j resolves successfully to the values of the i and j monitor scope variables. The second use occurs after the local declaration and initialization of i and j. That use resolves to the local (within the action) occurrence. This results in the following values:

  • Global variable i is set to 10.
  • Local variable i is set to 2.
  • Global variable j is set to the value of a.
  • Local variable j is set to 5.

Since you must explicitly initialize local variables before you can use them, the following example is invalid because j and i are not initialized to any value before they are used.

action anAction3(integer a) returns integer {
   integer i;
   integer j;
   return j + i; // error, i and j were not initialised
}

It is possible to initialize a variable on the same line as its declaration, as follows:

action anAction4(integer a) returns integer {
   integer i := 10;
   integer j := a;
   return j + i;
}

It is also possible to initialize a local variable by coassigning to it in an event listener. For example, the following is correct:

action onload() {
   on all Event() as e {
      log e.toString();
   }
}

You can also initialize a local variable by coassigning to it from a stream. For example:

action onload() {
   from x in all X() select x.f as f {
      log f.toString();
   }
}

Using variables in listener actions

Suppose you use a local variable in a listener action, as in the following example:

monitor MyMonitor {

   integer x;

   action onload() {
      integer y := 10;
      on all StockTick(*,*) {
         log x.toString();
         log y.toString();
      }
      y := 5;
   }
}

In this example, x is a global variable, and y is a local variable. There are references to both variables in the listener action.

A reference to a global variable in a listener action is the same as a reference to a global variable anywhere else in the monitor. However, a reference to a local variable in a listener action causes the correlator to retain a copy of the local variable for use when the event listener triggers. The value held by this copy is the value that the local variable has when the correlator instantiates the event listener.

When the event listener triggers the correlator executes the listener action. This will be at some point in the future, and after the rest of the body of the enclosing action has been executed. Since the action has already been executed, any of the original local variables no longer exist. This is why the correlator retains a copy of the local variable to make available to the listener action when it is executed.

In the example above, when the event listener triggers and the correlator executes the listener action

  • x has a value of 0, which is the value that the correlator automatically assigns
  • yhas a value of 10, which is the value it was set to when the event listener was instantiated

The value of y that the correlator retained when it instantiated the event listener is not affected by the subsequent statement (after the on statement) that sets the value of y to 5.

Info
For reference types (see also Reference types), retaining as a copy of the variable really means only retaining as a copy of its reference. Hence, if any code changes the contents of the referenced object(s) between event listener creation and event listener triggering, then this does affect the values used by the triggered event listener.

Specifying named constant values

In a monitor or in an event type definition, you can specify a named boolean, decimal, float, integer, or string value as constant. The format for doing this is as follows:

constant type name := literal;
Element Description
type Specify boolean, decimal, float, integer, or string. This is the type of the constant value.
name Specify an identifier for the constant. This name must be unique within its scope — monitor, event, or action.
literal Specify the value of the constant. The type of the value must be the type that you specify for the constant.

Benefits of using constants include:

  • Using a named constant can often be better than using a literal because it lets you define that constant in a single place. There is no chance of one instance becoming incorrect when the value is changed elsewhere. An alternative to using a constant would be to define a variable to contain the value. The disadvantage with this approach is that someone could accidentally assign a new value to the “constant”, which would cause errors.
  • A named constant can make code easier to read because the name can be meaningful in a way that a magic number, such as 42, is not.
  • Constants appear in memory once. For example, spawning multiple copies of a monitor that contains a constant does not consume memory to store extra copies of the constant. A non-constant variable takes up space in memory for every copy of the event or monitor in the correlator.

You can refer to a declared constant in any code in the event or monitor being defined. When you define a constant in an event you can refer to it from outside the event by qualifying the name of the constant with the event name, for example, MyEvent.myConstant.

Following is an example of specifying and using a constant:

event Paper {
   constant float GOLDEN := 1.61803398874;
   float width;
   action getLength() {
      return GOLDEN * width;
   }
   action getWidth() {
      return width;
   }
}

You cannot declare a constant in an action.

Defining actions

Actions are similar to procedures.

A monitor can define any number of actions. Finding an event, or pattern of events, of interest can trigger an action.

You can also trigger an action by invoking it from inside another action. You can also declare an action as part of an event type definition, and then call that action on an instance of that event.

The topics below provide information about defining actions.

Format for defining actions

The format for defining an action that takes no parameters and returns no value is as follows:

action actionName() {
   // do something
}

Optionally, an action can do either one or both of the following:

  • Accept parameters
  • Return a value

The format for defining an action that accepts parameters and returns a value is as follows:

action actionName(type1 param1, type2 param2,...) returns type3 {
   // do something
  return type3_instance;
}

For example:

action complexAction(integer i, float f) returns string {
   // do something
  return "Hello";
}

An action that accepts input parameters specifies a list of parameter types and corresponding names in parentheses after the action name. Parentheses always follow the action name, in declarations and calls, whether or not there are any parameters. Parameters can be of any valid EPL type. The correlator passes primitive types by value and passes complex types by reference. EPL types and their properties are described in the API reference for EPL (ApamaDoc).

When an action returns a value, it must specify the returns keyword followed by the type of value to be returned. In the body of the action, there must be a return statement that specifies a value of the type to be returned. This can be a literal or any variable of the same type as declared in the action definition.

An action can have any name that is not a reserved keyword. Actions with the names onload(), onunload() and ondie() can only appear once and are treated specially as already described in About monitor contents. It is an EPL convention to specify action names with an initial lowercase letter, and a capital for each subsequent word in the action name.

Actions and global variables must not have the same names. See Using action type variables. If you have any code that uses the same identifier for an action and a global variable, you must change it.

Invoking an action from another action

To invoke an action from another action, specify the action name followed by parentheses. If the action takes one or more input parameters, specify values for the parameters inside the parentheses. For example:

// First action:
action myAction1() {
   myAction2();
}

// Second action that is called by the first action:
action myAction2() {
   //...
}

In the example above, myAction1() calls myAction2() from inside the myAction1() declaration block. myAction2() takes no parameters and does not return a value.

When an action returns a value, you can invoke that action only from within an expression. You cannot specify a standalone statement that invokes an action that returns a value. Discarding the return value is illegal in EPL. For example:

action myAction3() returns string {
   return "Hello";
}

action myAction4() {
   string response;
   response := myAction3(); // Valid
   myAction3();             // Invalid
}

Consider this extended example:

// First action:
//
action myAction1() {
   myAction2();
}

// Second action that is called by the first action:
//
action myAction2() {
   string answer1, answer2;
   myAction5(5, 10.5);
   on anEvent() myAction5(5, 10.5);
   answer1 := myAction6(256, 1423.2);
   answer2 := myAction7();
}

// Action that is called by myAction2:
//
action myAction5 (integer i, float f) {
...
}

// Another action that is called by myAction2:
//
action myAction6 (integer i, float f) returns string {
   return "Hello";
}

// Yet another action that is called by myAction2:
//
action myAction7() returns string {
   return "Hello again";
}

myAction2() takes no parameters and does not return a value.

myAction5() accepts input parameters. You can invoke it from a standalone statement:

myAction5(5, 10.5);

You can also invoke it as a listener action:

on anEvent() myAction5(5, 10.5);

myAction6() accepts input parameters and returns a value. You can invoke myAction6() only from within an expression:

answer1 := myAction6(256, 1423.2);

myAction7() returns a value but does not take any parameters. You can invoke it only from within an expression:

answer2 := myAction7();

Specifying actions in event definitions

You can specify an action in an event type definition. This lets you call that action on an instance of the event, just as you would call a built-in method on some other type, such as calling the toString() method on the integer type.

When you define an action in an event, it behaves almost the same way as an action in a monitor. For example, an action in an event can

  • Set up event or stream listeners (only in a monitor)
  • Call other actions within that event
  • Access members of that event

In a monitor, an action in an event has an implicit self argument that refers to the event instance that the action was called on. The self argument behaves in the same way as the this argument in C++ or Java.

Example

For example, consider the following event type definition:

event Circle {
   action area() returns float {
      return 3.14159 * radius * radius;
   }
   action circumference() returns float {
      return 2.0 * 3.14159 * self.radius;
   }
   float radius;
}

The specifications here of radius and self.radius are equivalent.

You can then write code that looks like this:

Circle c := Circle(4.0);
print "Circle area = " + c.area().toString();
print "Circle circumference = " + c.circumference().toString();

Of course, the output is as follows:

Circle area = 50.26544
Circle circumference = 25.13272

Behavior

The correlator never executes actions in events automatically. In an event, if you define an onload() action, the correlator does not treat it specially as it does when you define the onload() action in a monitor.

When you call an action in an event, the correlator executes the action in the monitor instance in which the call was made. In a monitor, if the action sets up any listeners, these listeners are in the context of this monitor instance. If this monitor instance dies, the listeners also die.

You can use plug-ins from within event actions. In the event definition, specify the import statement to give the plug-in an alias within the event. Specify the import statement in the same way that you specify it for a monitor. You use the plug-in alias to call functions on the plug-in in the same way as you use it for a monitor.

When you define an event, there are no ordering restrictions for the definition of fields, imports, or actions. You can define them in any order.

Spawning

From an action within an event, you can spawn to an action in the same event. The correlator spawns a monitor instance and executes the specified action on the event instance in the new monitor instance.

It is not possible to spawn from outside a particular event to an action that is a member of that particular event. Instead, spawn to an action that calls the action that is the event member. For example:

event E {
   action spawntotarget() {
      spawn target();                         // legal
   }
   action target() {
      log "Spawned "+self.toString();
   }
}

monitor m {
   action onload() {
      E e;
      spawn e.target();                     // not legal
      spawn calltarget(e);                  // legal
      e.spawntotarget();
   }
   action calltarget(E e) {
      e.target();
   }
}

Be sure to follow the spawn keyword with an action name identifier. Actions spawned to must have no return value, as before. See also Utilities for operating on monitors.

Restrictions

To summarize, when you define an action in an event, the following restrictions apply:

  • If the action contains an on statement, you can coassign a matching event only to local variables. You cannot coassign a matching event to the event’s fields nor to items outside the event or in the monitor.
  • In a monitor, if you declare an instance of an event that has an action member, you cannot specify a call from that action to an action that is defined in the monitor.
  • You cannot assign values to the implicit self parameter, any more than you can assign to this in Java.

Using action type variables

In addition to defining an action, you can define a variable whose type is action. This lets you assign an action to an action variable of the same action type. An action is of the same type as an action variable if they have the same argument list (the same types in the same order) and return type (if any).

Defining action variables

The format for defining an action type variable is as follows:

action<[type1[, type2]...]>[returns type3]name;

Specify the keyword, action.

Follow the action keyword with zero, one or more parameter types enclosed in angle brackets and separated by commas. The angle brackets are required even when the action takes no arguments.

Optionally, follow the parameter list with a returns clause. Specify the returns keyword followed by the type of the returned value.

Finally, specify the name of the variable. For example:

action<string> a;
action<integer, integer> returns string b;

You can use an action variable anywhere that you can use a sequence or dictionary variable. For example, you can

  • Pass an action as a parameter to another action.
  • Return an action from execution of an action.
  • Store an action in a local variable, global variable, event field, sequence, or dictionary.

You cannot route, emit, enqueue or send an event that contains an action variable field.

You must initialize an action variable before you try to invoke it.

When an action variable is a member of an event the behavior of the action depends on the instance of the event that the action is called on. Consequently, it can be handy to bind an action variable member with a particular event instance. See Creating closures.

Built-in methods are treated exactly the same as user-defined actions. This means you can assign a built-in method to an action variable. For example:

action<float> returns string f := float.toString;

Invoking action variables

The only operation that you can perform on an action variable is to call it. You do this in the normal way by passing a set of parameters in parentheses after an expression that evaluates to the action variable. For example:

monitor Test{
   integer i;
   action<string> x; // Uninitialized global action variable.
   action onload() {

      // Invoke the runMe action. The first argument to runMe is an
      // action variable for an action having a single argument of
      // type integer and no return value.
      // Since the printInteger action conforms to the argument
      // expected by runMe, you can pass printInteger to runMe.
      runMe(printInteger, 10);

      // Declare a local action variable, g. This action takes one
      // integer argument and does not return a result.
      // The printInteger action conforms to this so
      // assign printInteger to g.
      action<integer> g := printInteger;

      // Invoke the runMe action again.
      // Pass g instead of explicitly passing printInteger.
      runMe(g, 20);

      // Declare a local dictionary that contains action variables.
      // Each action variable takes a single integer argument and
      // and does not return a result.
      // Add printInteger to the dictionary.
      // Invoke printInteger and pass 30 as the argument.
      dictionary<string, action<integer>> do := {};

      do["printIt"] := printInteger;
      do["printIt"] (30);

      // Invoke x. Since this global variable was never
      // initialized, the monitor instance terminates.
      x("hello!");
   }

   action runMe(action<integer> f, integer i) {
      f(i);
   }

   action printInteger(integer i) {
      print i.toString();
   }
}

After injection, this monitor prints

10
20
30

and then terminates upon invocation of x because x was never initialized.

Calling an uninitialized, local action variable causes an error that prevents the correlator from injecting the monitor. While the correlator injects code that contains an uninitialized, global action variable, trying to call the uninitialized variable causes a runtime error and the monitor instance terminates.

Declaring action variables in event definitions

When you define an action as a member field in an event, that action has an implicit self argument as the first argument (see Specifying actions in event definitions). You must include this implicit argument when determining whether an action definition conforms to an action variable declaration. For example, the following is illegal:

event A {
   action foo(float f) returns string {
      return "Hello";
   }
   action bar() {
      action<float> returns string f := A.foo;
   }
}

In the previous code, you cannot assign the A.foo action to f because f takes a single float argument whereas A.foo has two arguments — the implicit A argument and then the float argument. To correct this example, specify A as the first action argument in the body of the bar action.

event A {
   action foo(float f) returns string {
      return "Hello";
   }
   action bar() {
      action<A, float> returns string f := A.foo;
   }
}

Actions in place of routed events

In some situations, you might find it more efficient to use action type variables instead of routing events. For example, suppose you implement a service that takes an action variable as one of its parameters. Now suppose that the service needs a response from an adapter or some other service before it can send a response. When ready, the service can respond with a routed event, but that means you have to set up an event listener for that event. Routing events and setting up event listeners is more expensive than invoking actions. So instead of routing and listening, the service can respond by invoking the action on the event that initiated the service request. For example:

Illustration of routed events

The following sample code uses a routed event. Following this code there is a sample that uses an action on an event.

event ServiceResponse {
   string requestId;
...
}

event Service {
   action doRequest( string requestId,... ) {
...
   // when asynchronous 'service actions' are complete
      route ServiceResponse( requestId,... );
   }
...
}

monitor Client {
   Service service;
   action onload() {
...
      string id :=...;
      on ServiceResponse( requestId=id )as r {
...
      }
      service.doRequest( id,... );
   }
}

The following sample code uses an action on a Client monitor:

event Service {
   action doRequest( action<... > callback,... ) {
...
      // when asynchronous 'service actions' are complete
      callback(... );
   }
...
}

monitor Client {
   Service service;
   action onload() {
...
      string id :=...;
      service.doRequest( onServiceResponse,... );
   }
   action onServiceResponse(...) {
...
   }
}

Creating closures

When an action is a member of an event the behavior of the action depends on the instance of the event that the action is called on. Consequently, you might want to bind an action member with a particular event instance. When you bind an action member to an event instance you are creating a closure. The advantages of creating a closure are:

  • Simpler syntax for executing the action
  • Greater flexibility in making assignments to action variables

Consider the following event definition:

event E {
   integer i;
   action foo() { print "Foo "+i.toString(); }
   action times(integer j) returns integer { return i*j; }
}

With this definition, E(1).foo() would print “Foo 1”, while E(42).foo() prints “Foo 42”. The action E.foo always has a specific instance of E to work with. You can achieve this by specifying the action’s implicit self argument when you call the action, as described earlier in this topic. When you use this technique you identify the event instance when you call the action variable.

Alternatively, you can create a closure that binds an action member with an event instance. You store the closure in an action variable. The action variable and the action member must be of the same action type. That is, they must take the same argument(s), if any, and return the same type, if any.

When you use this technique you identify the event instance when you assign the event’s action member to the action variable.

The following code shows an example of binding an event instance to an action member by storing the closure in an action variable.

monitor m {
   action <> a;
   action onload() {
      E e := E(42);
      a := e.foo;
      a(); // Prints "Foo 42"
   }
}

In this example, e.foo denotes E.foo called on e. That is, when you assign the action e.foo to the a action variable you are identifying which instance of E to use when you call the a action. This closure binds a reference to E to the E.foo action and stores it in the a action variable. After you create a closure, you can call an action on an event as though it is a simple action. This gives you considerable flexibility in what you can assign to an action variable.

More about closures

EPL performs its own garbage collection. Consequently, you do not need to consider how long a bound object must last. This is handled automatically.

A closure binds by reference. Consider the following example, which uses the same event E as above:

monitor m {
   action <integer> returns integer a;
   action onload() {
      E e := E(3);
      a := e.times;
      print a(2).toString(); // Prints "6"
      e.i := 5;
      print a(2).toString(); // Prints "10"
   }
}

In a portion of code, you can define multiple action variables that contain closures for the same object. For example:

event Counter {
   integer i;
   action increment() { i := i+1; }
   action output() { print i.toString(); }
}
event Increment {}

event Finish {}

monitor m {
   action <> incrementAction;
   action <> outputAction;
   action onload() {
      Counter counter := new Counter;
      incrementAction := counter.increment;
      outputAction := counter.output;
      on all Increment() and not Finish() { incrementAction(); }
      on all Finish() { outputAction(); }
   }
}

In an event type, when an action member refers to another action member in the same event type a closure happens implicitly. For example:

event E {
   action <integer> returns integer a;
}

event Plus {
   integer i;
   action f(integer j) returns integer { return i+j; }
   action setA(E e) { e.a := f; }
}

Here, the f in e.a := f is equivalent to self.f, just as it would be if setA had called f instead of assigning it to an action variable. This creates a closure. After setA is called on some instance of Plus, e.a will call f on that same instance.

Other ways to specify closures

You can create a closure using any value and any action on that value. Thus, it is possible to:

  • Bind a built-in method to a value.
  • Bind actions to primitive types and other reference types instead of to events.
  • Bind actions to a literal or a function’s return value instead of a variable’s value.

For example:

// Print "E(42)"
E e := E(42);
action <> printE42 := e.toString;

// Print "Foo 12345"
action <> printFoo12345 := E(12345).foo;

// Take a floating-point number and return e to that power:
action <float> returns float eToTheX := 2.718282.pow;

// Return a random integer from 0 to 9 inclusive.
// (The brackets around 10 are needed so that "10." is not treated as a
// floating-point number.)
action <> returns integer randomDigit := (10).rand;

// Return the strings in a sequence, separated by colons.
action <sequence<string>> returns string j := ":".join;

Restrictions

You cannot route, enqueue, emit or send an event that contains an action variable field. It is okay to route, enqueue, emit or send an event that contains an action definition.

An action variable cannot be a key in a dictionary. An event that contains an action field cannot be a key in a dictionary.

Defining static actions

In contrast to the regular actions that are described in Defining actions, static actions do not apply to specific instances of an event. They are not as powerful as regular actions, but they are helpful in situations where it makes more sense to use an action that is related to the event type, and where the action is not called on an instance of that event.

Static actions can only be declared inside an event type. They are defined in just the same way as regular actions (see Format for defining actions). The only differences are that they start with static, cannot reference self, and cannot reference members of the event. For example:

static action staticActionName() {
   // do something
}

The following example contrasts a regular action with a static action.

event MyEventType {
    integer i;

    action someAction() {
        print i.toString(); // Valid
    }

    static action someStaticAction() {
        print i.toString(); // Not valid
        someAction();       // Not valid
    }
}

MyEventType e := new MyEventType;
e.someAction();

MyEventType.someStaticAction();

A static action can be used, for example, if you have a factory action which constructs a new instance of a particular event type and initializes its members to values that make sense for that type. Although it is possible to have such code in any place, in terms of program readability, it is more helpful to “associate” the static action with the event type that it is creating. For example:

event MyEventType {
    string s;
    integer i;

    static action initialise() returns MyEventType {
        MyEventType ret := new MyEventType;
        ret.s := "Default";
        ret.i := 100;
        return ret;
    }

...
}

MyEventType e := MyEventType.initialise();

With the above definition, the static action can then be called using a single line of code anywhere in your program.

Getting the current time

In the correlator, the current time is the time indicated by the most recent clock tick. However, there are some exceptions to this:

  • If you specify the -Xclock option when you start the correlator, the correlator does not generate clock ticks. Instead, you must send time events (&TIME) to the correlator. The current time is the time indicated by the most recent received, externally generated, time event. See Externally generating events that keep time (&TIME events).
  • If you have multiple contexts, it is possible for the current time to be different in different contexts. A particular context might be doing so much processing that it cannot keep up with the time ticks on its queue. In other words, if contexts are mostly idle, then they would all have the same current time.
  • When the correlator fires a timer, the current time in the context that contains the timer is the timer’s trigger time. See About timers and their trigger times.

The information in the remainder of this topic assumes that the current time is the time indicated by the most recent clock tick.

Use the currentTime variable to obtain the current time, which is represented as seconds since the epoch, January 1st, 1970 in UTC. The currentTime variable is similar to a global read-only constant of type float. However, the value of the currentTime variable is always changing to reflect the correlator’s current time.

In the correlator, the current time is never the same as the current system time. In most circumstances it is a few milliseconds behind the system time. This difference increases when the input queues of public contexts grow.

When a listener executes an action, it executes the entire action before the correlator starts to process another event. Consequently, while the listener is executing an action, time and the value of the currentTime variable do not change. Consider the following code snippet,

float a;
action checkTime() {
   a := currentTime;
}

//... Lots of additional code
// A listener calls the following action some time later
action logTime() {
   log a.toString(); // The time when checkTime was called
   log currentTime.toString(); // The time now
}

In this code, an event listener sets float variable a to the value of currentTime, which is the time indicated by the most recent clock tick. Some time later, a different event listener logs the value of a and the value of currentTime. The values logged might not be the same. This is because the first use of currentTime might return a value that is different from the second use of currentTime. If the two event listeners have processed the same event, the logged values are the same. If the two event listeners have processed different events, the logged values are different.

Generating events

As discussed previously, actions can perform calculations and log messages. In addition, actions can dynamically generate events. The topics below discuss this.

Generating events with the route statement

The route statement generates a new event that goes to the front of the input queue of the current context.

Any active listeners seeking that event then receive it. There is only one difference between an externally sourced event (passed in through a live message feed) and an event that was generated internally through a route statement. The difference is that internally routed events are placed at the front of the context’s input queue in the same order as they are routed within an action, and after any previously internally routed events where multiple listener actions have been triggered by an event. The correlator processes the routed events on the input queue before it processes the next non-routed event on the input queue. See Event processing order for monitors.

For example:

action simulateCrash() {
  route StockTick(currentStock.name, 50.0);
  route StockTick(currentStock.name, 30.0);
  route StockTick(currentStock.name, 20.0);
  route StockTick(currentStock.name, 10.0);
  route StockTick(currentStock.name, 5.0);
  route StockTick(currentStock.name, 1.0);
}

The simulateCrash() action shown above routes six StockTick events for the monitor’s specific stock name, with drastically reducing prices. Other monitors (or the same monitor) may receive these events and process them accordingly.

The route statement can operate on any values as well as events, provided that the any value is of a routable event type.

You cannot route an event if the event (or one of its members) contains a field of an unroutable type (action, chunk, listener, stream). There is a runtime check if the event (or one of its members) can contain an any field; an exception is thrown if the any field contains an object of type action, chunk, listener, or stream.

Note that you can route an event whose type is defined in a monitor.

Generating events with the send statement

The send statement sends an event to a channel, a context, a sequence of contexts, or a com.apama.Channel object.

When you send an event to a channel, the correlator delivers it to all contexts and external receivers that are subscribed to that channel. To send an event, use the following format:

send event_expression to expression;

The result type of event_expression must be an event. It cannot be a string representation of an event. The send statement can operate on any values as well as events, provided that the any value is of a routable event type.

To send an event to a channel, the expression must resolve to a string or a com.apama.Channel object that contains a string. If there are no contexts and no external receivers that are subscribed to the specified channel, then the event is discarded. See Subscribing to channels.

The only exception to this is the default channel, which is the empty string. Events sent to the default channel go to all public contexts.

To send an event to a context, the expression must resolve to a context, a sequence of contexts, or a com.apama.Channel object that contains a context. You must create a context before you send an event to the context. You cannot send an event to a context that you have declared but not created. For example, the following code causes the correlator to terminate the monitor instance:

monitor m {
   context c;
   action onload()
   {
      send A() to c;
   }
}

If you send an event to a sequence of contexts and one of the contexts has not been created first, then the correlator terminates the monitor instance. Sending an event to a sequence of contexts is non-deterministic. You cannot send an event to a sequence of com.apama.Channel objects. For details, see Sending an event to a sequence of contexts.

All routable event types can be sent to contexts, including event types defined in monitors. There is a runtime check if the event (or one of its members) can contain an any field; an exception is thrown if the any field contains an object of type action, chunk, listener, or stream.

If a correlator is configured to connect to Universal Messaging, then a channel might have a corresponding Universal Messaging channel. If there is a corresponding Universal Messaging channel, then Universal Messaging is used to send the event to that Universal Messaging channel.

See Choosing when to use Universal Messaging channels and when to use Apama channels.

Sending events to com.apama.Channel objects

A com.apama.Channel object is particularly useful when writing services that can be used in both distributed and local systems. For example, by using a Channel object to represent the source of a request, you could write a service monitor so that the same code sends a response to a service request. You would not need to have code for sending responses to channels and separate code for sending responses to contexts.

Consider the following Request event and Service monitor definitions:

event Request {
   ...
   Channel source;
}

monitor Service {
   action onload() {
      monitor.subscribe("Requests");
      on all Request() as req {
         Response rep := Response(...);
         send rep to req.source;
      }
   }
}

EPL code in a context in the same correlator as the Service monitor could send a Request event with the source field set to context.current() and would receive the Response event that the Service monitor sends. For example:

monitor LocalRequester {
   action onload() {
      Request req := Request(...);
      req.source := Channel(context.current());
      send req to "Requests";

      on all Response() as rep {
      ...
      }
   }
}

Now consider a monitor that is in a correlator that is connected to the Service monitor host correlator. For example, the correlators can be connected by means of engine_connect. The remote monitor could send a Request event with the source field set to a Channel object that contains the name of a channel that the remote monitor is subscribed to. For example:

monitor RemoteRequester {
   action onload() {
      monitor.subscribe("Responses");

      Request req := new Request;
      req.source := Channel("Responses");
      send req to "Requests";

      on all Response() as rep {
     //...
     }
   }
}

In this example, if the correlators are connected by means of engine_connect then the connections would need to be subscribed to the Requests channel and the Responses channel. As you can see, the service monitor does not require different code according to whether the request is coming from a local or remote context. The service monitor simply sends the response back to the source and it does not matter whether the source is a context or a channel.

You can send a Channel object from one Apama component to another Apama component only when the Channel object contains a string. You cannot send a Channel object outside a correlator when it contains a context.

Enqueuing to contexts

To enqueue an event to a particular context, use the enqueue...to statement:

enqueue event_expression to context_expression;
Info
The enqueue...to statement is superseded by the send...to statement. The enqueue...to statement will be deprecated in a future release. Use the send...to statement instead. See Generating events with the send statement.

The result type of event_expression must be an event. It cannot be a string representation of an event. The result type of context_expression must be a context or a variable of type context. It cannot be a com.apama.Channel object that contains a context. The enqueue...to statement can operate on any values as well as events, provided that the any value is of a routable event type.

The enqueue...to statement sends the event to the context’s input queue. Even if you have a single context, a call to enqueue x to context.current() is meaningful and useful.

You must create the context before you enqueue an event to the context. You cannot enqueue an event to a context that you have declared but not created. For example, the following code causes the correlator to terminate the monitor instance:

monitor m {
   context c;
   action onload()
   {
      enqueue A() to c;
   }
}

If you enqueue an event to a sequence of contexts and one of the contexts has not been created first then the correlator terminates the monitor instance. For details, see Sending an event to a particular context.

Sending an event to a sequence of contexts is non-deterministic.

All routable event types can be enqueued to contexts, including event types defined in monitors. There is a runtime check if the event (or one of its members) can contain an any field; an exception is thrown if the any field contains an object of type action, chunk, listener, or stream.

Generating events to emit to outside receivers

The emit statement dispatches events to external registered event receivers, which means that the events leave the correlator. Active listeners do not receive emitted events.

Info
The emit statement is superseded by the send statement. See Generating events with the send statement. The emit statement will be deprecated in a future release. Use send rather than emit.

There are two formats available for using emit. You can directly emit an event, as the example below does first, or else place the event in a string and emit that. If you use this latter format, you must ensure that you define the string to represent a valid event. The correlator does not check whether the string you specify represents an event that is compliant with any event type that has been injected. In fact, you can use this mechanism to emit an event of a type that has not been defined in EPL anywhere else.

For example, consider a revised version of an earlier example. The result, instead of being printed as a message on the screen, is now being sent out as an event message:

event StockTickPriceChange {
   string owner;
   string name;
   float price;
}

// A new processTicks action that dispatches an output event
// to external applications instead of logging
action processTicks() {

// The following emit format sends the event itself.
   emit StockTickPriceChange(currentStock.owner,
      newTick.name, newTick.price) to
      "com.apamax.pricechanges";

// Or, use the following emit format, which sends a string that
// contains the event.
   emit "StockTickPriceChange(\""+currentStock.owner+
      "\",\""+newTick.name+"\", "+newTick.price.toString()+")" to
      "com.apamax.pricechanges";

Events are emitted onto named channels. In the above code the StockTickPriceChange event is being published on the com.apamax.pricechanges channel. For an application to receive events from Apama it must register itself as an event receiver and subscribe to one or more channels. Then if events are emitted to those channels they will be forwarded to it.

Channels effectively allow both point-to-point message delivery as well as through publish-subscribe. As in the above example, channels can be set up to represent topics. External applications can then subscribe to event messages of the relevant topics. Otherwise a channel can be set up purely to indicate a destination and have only one application connected to it.

The emit statement can operate on any values as well as events, provided that the any value is of a routable event type.

You cannot emit the following events:

  • An event whose type is defined inside a monitor.
  • An unroutable event type. There is a runtime check if the event (or one of its members) can contain an any field; an exception is thrown if the any field contains an object of type action, chunk, listener, or stream.

If a correlator is configured to connect to Universal Messaging, then a channel might have a corresponding Universal Messaging channel. If there is a corresponding Universal Messaging channel, then Universal Messaging is used to emit the event to that Universal Messaging channel.

See Choosing when to use Universal Messaging channels and when to use Apama channels.

Handling the any type

EPL supports an any type that can hold a value of a concrete EPL type (that is, a type other than the any type). See the API reference for EPL (ApamaDoc) for full details of the any type.

The switch statement is the preferred way of handling any values unless the type is known or not important. See Handling any values of different types with the switch statement for details on the switch statement.

An any value may be empty and not contain a value, or it can contain a value which has a type associated with it. The type of the value can be obtained using the getTypeName() method on the any type.

A variable of a concrete type can be used where an any value is expected in:

  • assignments and initialization, for example:

    any anyVariable := "string value";
    
  • return values, for example:

    action a() returns any { return new sequence<integer>; }
    
  • passing a parameter to an action, for example:

    actionWithAnyParameter("string value");
    
  • the index of a dictionary with an any key type.

In these cases, the concrete type is automatically converted to the any type. This is always safe and valid, and will not throw an exception.

Reflection on types

Reflection allows EPL to act on values of any type in a generic way, including altering the behavior to adapt to what fields or actions a type has. This can be values passed as an any parameter value to some common code, matching an any() listener (see Listening for events of all types), or created via the any.newInstance method.

Fields or entries from an any value can be accessed using the following methods:

  • setEntry(any key, any value)
  • getEntry(any key) returns any
  • getEntries() returns sequence

For event types, the key should be a string containing the field name. For sequences, key is the index and should have an integer value.

The actions (including methods) and constants can be obtained with the following methods of the any type:

  • getActionNames() returns sequence
  • getAction(string name) returns any
  • getConstant(string) returns any
  • getConstantNames() returns sequence

Actions may be cast to the correct action type and then invoked directly.

For actions, a list of the action’s parameter names, a dictionary mapping from the parameter name to the parameter type, and the name of the return type can be obtained with the following methods of the any type:

  • getActionParameterNames() returns sequence<string>
  • getActionParameters() returns dictionary<string,string>
  • getActionReturnTypeName() returns string

For actions, it is also possible to use a “generic” form to call the action via the following method of the any type, even if the signature type is not known at compile time:

  • getGenericAction() returns action<sequence<any>> returns any

A sequence<any> of the parameter values of the correct count and types must be supplied.

For detailed information on the above methods, see the any type in the API reference for EPL (ApamaDoc).

Cast operations

EPL supports casting of the any type to a concrete target type and vice versa.

  • Casting to the any type

    Casting a concrete type is allowed to any only. The cast is redundant in this case. Example:

    integer i := 10;
    
    any a := <any> i;   //redundant cast
    any a2 := i;        //valid
    

    Example of a cast that is not redundant:

    sequence<any> entries := (<any> evt).getEntries();
    
  • Casting to a concrete type

    targetType tgtValue := <targetType> anyValue;
    

    Casting an any type with an empty value throws Exception with type set to CastException. See also the Exception type in the API reference for EPL (ApamaDoc).

    If the anyValue does not contain an object of targetType, it throws Exception with type set to CastException, and with the message mentioning the actual type contained by anyValue and targetType. Exceptions to this rule are the following casts, which are valid:

    • any(integer) to float
    • any(integer) to decimal
    • any(float) to decimal
    • any(decimal) to float Examples:
    any a := 10;
    
    integer i := <integer> a; // Valid
    
    // Will inject but throws a CastException during runtime.
    string s := <string> a;
    
  • Casting to the optional type

    optional<targetType> opt  := <optional<targetType>> anyValue;
    

    Casting to optional<targetType> will never throw. If the any value cannot be converted, then an empty optional<targetType> is returned instead.

    If the anyValue is empty, the cast returns an empty optional<targetType>.

    If anyValue contains object of optional<targetType> type, the cast returns that object of type optional<targetType>.

    If the anyValue contains an object of targetType, the cast returns object of type optional<targetType> containing targetType.

    If the anyValue does not contain an object of targetType or optional<targetType>, the cast returns an empty optional<targetType>.

    Examples:

    any a := 10;
    
    // Returns optional <integer> containing the value 10.
    optional<integer> opti := <optional<integer>> a;
    
    // Returns an empty optional <string>.
    optional<string> opts := <optional<string>> a;
    

Handling any values of different types with the switch statement

The switch statement is used to conditionally execute a block of code. Unlike the if and if... else statements, the switch statement can have a number of possible execution paths.

The switch statement operates on an expression of the any type (see also Handling the any type). At runtime, the type of the value is examined, and the case clause for that type is executed if there is one, otherwise the default clause is executed. If the any value is empty, the default clause is always executed.

If the default clause is not present and none of the case clauses match the type of the value passed to the switch statement, then the switch statement throws Exception with type set to UnmatchedTypeException. See also the description of the Exception type in the API reference for EPL (ApamaDoc).

The switch statement names the expression as an identifier with the as keyword followed by an identifier to name the value. In each case clause block, the identifier has the same type as the case clause.

If the expression is a simple identifier (that is, it is referring to a variable or parameter), then the as Identifier part can be omitted. The new local retains the same name.

The following code example shows the usage of the switch statement in an action which returns a string, where the case clauses use return statements to return from the action. The identifier value in each clause has the same type as the case clause.

Example:

action getStringOrBlank(any value) returns string {
   switch(value) {

        // value will be of type float in this block
        case float : { return "float "+ value.toString(); }

        // value will be of type string in this block
        case string: { return value; }

        // value will be of type decimal in this block
        case decimal: { return "decimal "+value.toString(); }

        // value will be of type integer in this block
        case integer: { return "integer: "+ value.toString(); }

        // value will be of type sequence<string> in this block
        case sequence<string> : { return " ".join(value); }

        // value will be of any type in this block
        default: { return ""; }

   }
}

See also The switch statement.

Assigning values

Valid examples of an assignment statement are:

integerVariable := 5;
floatVariable := 6.0;
stringVariable := "ACME";
stringVariable2 := stringVariable;

Assignments are only valid if the type of the literal or variable on the right hand side corresponds to the type of the variable on the left hand side, or can be implicitly converted. Implicit conversions are allowed when assigning to an any type, or to an optional (provided the contained type of the optional matches the value being assigned).

When doing an assignment from a variable to another variable, the behavior of EPL depends on the type of the variable.

  • In the case of primitive types, the variable on the left hand side is set to the same value as the variable on the right hand side. The value is therefore copied and the two variables remain distinct.
  • In the case of complex reference types, the variable on the left hand side is set to reference the same object as the variable on the right hand side. Only the reference is copied, while the underlying object remains the same. If the object is subsequently changed, both variables would reflect the change.
  • In the case of the any type, setting to a primitive type creates a new object automatically to hold the primitive value. This is transparent to EPL, but is significantly more expensive than a simple primitive to primitive assignment.

Defining conditional logic with the if statement

EPL supports conditional if and if... else statements.

An if statement is followed by a boolean expression followed by an optional then keyword followed by a block. A block consists of one or more statements enclosed in curly braces, { }. If the boolean expression is true, the contents of the block are executed.

The boolean expression must evaluate to the boolean values true or false.

The if statement can be optionally followed by an else keyword and a second block. This second block is executed if the boolean expression is false. Instead of the else block, a single if statement, not enclosed in braces, may be used.

EPL example:

if floatVariable > 5.0 {
   integerVariable := 1;
}  else if floatVariable < -5.0 {
      integerVariable := -1;
}  else {
      integerVariable := 0;
}

Defining conditional logic with the ifpresent statement

The ifpresent statement is used to check if one or more values are empty (that is, whether they have a value or not). It unpacks the values into new local variables and conditionally executes a block of code.

The ifpresent statement is followed by one or more expressions with an optional name of a target variable followed by a block of code. If each expression is not empty, then the value of the expression is placed in a new local variable whose name is supplied after the keyword as. If the expression is a simple identifier (that is, it is referring to a variable or parameter), then the as identifier part can be omitted; a new local variable (which shadows the original variable) is created with the same name. Multiple expressions can be used in a single ifpresent statement, separated by commas.

If all the expressions have non-empty values, then the first block is executed. A block consists of one or more statements enclosed in curly braces, { }. The new local variables are only available in the first block of the ifpresent statement, where they are guaranteed to have non-empty values.

ifpresent can be optionally followed by an else keyword and block, which is executed if any of the supplied expressions do not have a value.

For optional types, the new local variable is of the unpacked type (the contained type of the optional), for example:

optional<integer> possibleNumber := 42;
ifpresent possibleNumber {
            // in this block, possibleNumber is of type integer,
            // so we can perform arithmetic on it:
            nextNumber := possibleNumber + 1;
} else {
            // in practice, won't be executed as possibleNumber
            // has been initialized with a value.
}

Thus, ifpresent is the recommended way of handling optional variable types. Usually, there is no need to call the getOrThrow method of the optional type. ifpresent combines the check of whether the value is empty with extracting the value and control flow, and thus reduces the amount of code that could throw an exception.

As an alternative to the ifpresent statement, you can use the getOr method of the optional type, which is less verbose than using ifpresent. This is helpful if you want to treat a missing (empty) value as having a value. For example, possibleNumber.getOr(0) will give you the number in possibleNumber, or 0 if it is empty.

ifpresent operates on expressions of the following types:

  • optional
  • chunk
  • stream
  • listener
  • context
  • action
  • any

See the API reference for EPL (ApamaDoc) for more information on these types.

See also The ifpresent statement.

Defining loops

EPL supports two loop structures, while and for.

An EPL example for while is:

integerVariable := 20;
while integerVariable > 10 {
   integerVariable := integerVariable – 1;
   on StockTick("ACME", integerVariable) doAction();
}

The for looping structure allows looping over the contents of a sequence. The counter must be an assignable variable of the same type as the type of elements of the sequence. For example:

sequence<integer> s;
integer i;
s.append(0);
s.append(1);
s.append(2);
s.append(3);
for i in s {
   print i.toString();
}

The loop will iterate through all the indices in the sequence, checking whether there are any more indices to cover each time. In the example above, i will be set to s[0], then s[1], and so on up to s[3]. The counter continues incrementing by one each time, and is checked to verify whether it is less than s.size() before a further iteration is carried out. Looping only terminates when the next index would be beyond the last element of the sequence, or equal to size() (since indices are counted from 0).

When the correlator executes a for loop, it operates on a reference to the sequence. Consequently, if the code in the for loop assigns some other sequence to the sequence expression specified in the for statement this has no effect on the iteration. However, if the code in the for loop changes the contents of the sequence specified in the for statement, this can affect the iteration. For example:

sequence <string> tmp := ["X", "Y", "Z"];
sequence <string> seq := ["A", "B", "C", "D", "E"];
string s;
for s in seq {
   seq := tmp;
   print s;
}

The for loop steps through whatever seq referred to when the loop began. Therefore, assigning tmp to seq inside the loop does not affect the behavior of the loop. This code prints A, B, C, D, and E on separate lines.

In the following example, the code in the for loop changes the contents of the sequence specified in the for statement and this affects the behavior of the loop.

sequence<string> seq := ["A", "B", "C", "D", "E"];
string s;
for s in seq {
   seq[2] := "c";
   print s;
}

This code prints A, B, c, D, and E on separate lines.

In the following code, the changes to the contents of the specified sequence would prevent the for loop from terminating.

sequence<string> seq := ["x"];
string s;
for s in seq {
   seq.append(s);
}

EPL provides the following statements for manipulating while and for loops. Usage is intuitive and as per other programming language conventions:

  • break exits the innermost loop. You can use a break statement only inside a loop.
  • continue moves to the next iteration of the innermost loop. You can use a continue statement only inside a loop.
  • return terminates both the loop and the action that contains it.

Exception handling

EPL supports the try... catch exception handling structure. The statements in each block must be enclosed in curly braces. For example:

using com.apama.exceptions.Exception;
...
action getExchangeRate(
   dictionary<string, string> prices, string fxPair) returns float {
   try {
      return float.parse(prices[fxPair]);
   } catch(Exception e) {
      return 1.0;
   }
}

Exceptions are a mechanism for handling runtime errors. Exceptions can be caused by any of the following, though this is not an exhaustive list:

  • Invalid operations such as trying to divide an integer by zero, or trying to access a non-existent entry in a dictionary or sequence
  • Methods that fail, for example trying to parse an object that cannot be parsed
  • Plug-ins
  • Operations that are illegal in certain states, such as spawn-to in an ondie() or onunload() action, or sending an event to a context and specifying a variable that has not been assigned a valid context object
  • The throw statement. See The throw statement for more information.

An exception that occurs in try block1 causes execution of catch block2. An exception in try block1 can be caused by:

  • Code explicitly in try block1
  • A method or action called by code in try block1
  • A method or action called by a method or action called by code in try block1, and so on.

Note that the die statement always terminates the monitor, regardless of try... catch statements.

The variable specified in the catch clause must be of the type com.apama.exceptions.Exception. Typically, you specify using com.apama.exceptions.Exception to simplify specification of exception variables in your code. The Exception variable describes the exception that occurred.

The com.apama.exceptions namespace also contains the StackTraceElement built-in type. The Exception and StackTraceElement types are always available; you do not need to inject them and you cannot delete them with the engine_delete utility.

An Exception type has methods for accessing:

  • A message — Human-readable description of the error, which is typically useful for logging.
  • A type — Name of the category of the exception, which is useful for comparing to known types to distinguish the type of exception thrown. Internally generated exceptions have types such as ArithmeticException and ParseException. For a list of exception types, see the description of the Exception type in the API reference for EPL (ApamaDoc).
  • A stack trace — A sequence of StackTraceElement objects that describe where the exception was thrown. The first StackTraceElement points to the place in the code that immediately caused the exception, for example, an attempt to divide by zero or access a dictionary key that does not exist. The second StackTraceElement points to the place in the code that called the action that contains the immediate cause. The third StackTraceElement element points to the code that called that action, and so on. Each StackTraceElement object has methods for accessing:
    • The name of the file that contains the relevant code
    • The line number of the relevant code
    • The name of the enclosing action
    • The name of the enclosing event, monitor or aggregate function

Information in an Exception object is available by calling these built-in methods:

  • Exception.getMessage()
  • Exception.getType()
  • Exception.getStackTrace()
  • StackTraceElement.getFilename()
  • StackTraceElement.getLineNumber()
  • StackTraceElement.getActionName()
  • StackTraceElement.getTypeName()

In the catch block, you can specify corrective steps, such as returning a default value or logging an error. By default, execution continues after the catch block. However, you can specify the catch block so that it returns, dies or causes an exception.

You can nest try... catch statements in a single action. For example:

action NestedTryCatch() {
   try {
      print "outer";
      try {
         print "inner";
         integer i:=0/0;
      } catch(Exception e) {
         // inner catch
      }
   } catch(Exception e) {
      // outer catch
   }
}

The block in a try clause can specify multiple actions and each one can contain a try... catch statement or nested try... catch statements. An exception is caught by the innermost enclosing try... catch statement, either in the action where the exception occurs, or walking up the call stack. If an exception occurs and there is no enclosing try... catch statement then the correlator logs the stack trace of the exception. If the throw is from an onload() or spawned action, from within a stream query or there is an ondie() defined in the monitor, then the monitor instance is terminated.

See About executing ondie() actions for information about how ondie() can optionally receive exception information if an instance dies due to an uncaught exception.

Logging and printing

The following operations are provided for debugging and textual output:

  • print string
  • log string [at identifier]

The print statement outputs its text to standard output, which is normally the active display or some file where such output has been piped. See also Strings in print and log statements.

The log statement sends the specified string to a particular log file depending on the applicable log level. For details, see Setting EPL log files and log levels dynamically.

The topics below provide information for using the log statement.

Specifying log statements

The format of a log statement is as follows:

log string [at identifier]

Syntax description

Syntax Element Description
string Specify an expression that evaluates to a string.
identifier Optionally, specify the desired log level. Specify one of the following values: CRIT, FATAL, ERROR, WARN, INFO, DEBUG or TRACE. If you do not specify an identifier, the default is INFO.

It is recommended that you do not use the FATAL or CRIT log levels, which are present only for historical reasons. It is better to use ERROR for all error conditions regardless of how fatal they are, and INFO for informational messages.

For each encountered log statement, the correlator compares the specified identifier with the applicable log level to determine whether to send the specified string to a log file. If the string is to be sent to a log file, the correlator determines the appropriate log file to send it to.

The correlator uses the tree structure of EPL code to identify the applicable log level and the appropriate log file. See Setting EPL log files and log levels dynamically.

Log levels determine results of log statements

The correlator supports the following log levels:

0

OFF

No entries go to log files.

1

CRIT

Least amount of entries go to log files.

2

FATAL

     |

3

ERROR

     |

4

WARN

     |

5

INFO

     |

6

DEBUG

     |

7

TRACE

Greatest amount of entries go to log files.

You use log levels to filter out log strings. If the log level in effect is lower than the log level in the `log` statement the correlator does not send the string to the log file. For example, if the log level in effect is `ERROR` \(3\) and the log level in the `log` statement is `DEBUG` \(6\) the correlator does not send the string to the log file since the log level in effect is lower than the log level in the `log` statement.

Suppose that a string expression in a log statement executes an action or has side effects. In this situation, the correlator executes the log statement so that side effects always take place. However, if the log level in effect is lower than the log level in the log statement the correlator still does not send the string to the log file.

Here are some examples where the log level in effect is WARN:

log "foo bar" at CRIT; // Sends "foo bar" to the log file.
log "foo bar" at INFO; // Does not send anything to the log file.

log "foo" + "bar" + 12345.toString() at INFO;
   // Does not send anything to the log file.
   // The expression in the log statement is not evaluated as
   // the log level is too low to send output to the log file,
   // and the expression does not have side effects.

log "foo" + bar() + 12345.toString() at INFO;
   // Does not send anything to the log file.
   // Calls bar() since that action might have side effects,
   // for example, the action could send an event.

Actions on events or monitors are assumed to have side effects. The com.apama.epl.SideEffectFree annotation (see Adding predefined annotations) can be added to an action definition to mark it as side effect free. Note that with this annotation, actions will only be called from log statements if the log statement would write to the log file. This is more compact than checking the log level before executing the log statement. If the action does in fact have side effects, then changing the log level can change the behavior of your program. It is recommended to only add the SideEffectFree annotation on an action if a profile shows that a lot of time is spent in calling that action (premature optimizations add to program complexity for no benefit). Actions called via an action variable are always assumed to have side effects, as the EPL runtime does not know which action is invoked.

For more information on the profile, see Profiling EPL Applications.

To determine the log level in effect, the correlator checks whether you set a log level for the following in the order specified below:

  1. The monitor or event that contains the log statement.
  2. A parent of the monitor or event that contains the log statement. The correlator starts with the immediate parent and works its way up the tree as needed.
  3. The correlator.

The log level in effect is the first log level that the correlator finds in the tree structure. See Setting EPL log files and log levels dynamically. If the correlator does not find a log level, the correlator uses the correlator’s log level. If you did not explicitly set the correlator’s log level, the default is INFO.

After the correlator identifies the applicable log level, the log level itself determines whether the correlator sends the log statement output to the appropriate log file as follows:

Log level in effect For log statements with these identifiers, the correlator sends the log statement output to the appropriate log file For log statements with these identifiers, the correlator ignores log statement output
OFF None CRIT, FATAL, ERROR, WARN, INFO, DEBUG, TRACE
CRIT CRIT FATAL, ERROR, WARN, INFO, DEBUG, TRACE
FATAL CRIT, FATAL ERROR, WARN, INFO, DEBUG, TRACE
ERROR CRIT, FATAL, ERROR WARN, INFO, DEBUG, TRACE
WARN CRIT, FATAL, ERROR, WARN INFO, DEBUG, TRACE
INFO CRIT, FATAL, ERROR, WARN, INFO DEBUG, TRACE
DEBUG CRIT, FATAL, ERROR, WARN, INFO, DEBUG TRACE
TRACE CRIT, FATAL, ERROR, WARN, INFO, DEBUG, TRACE None

An advantage of this framework is that there is no performance penalty for having log statements that do not specify actions in your application. You control the overhead of executing such log statements by specifying the appropriate log level.

Where do log entries go?

When the correlator needs to send the log statement output to a log file, the correlator checks whether you set a log file for the following in the order specified below:

  1. The monitor or event that contains the log statement.
  2. A parent of the monitor or event that contains the log statement. The correlator starts with the immediate parent and works its way up the tree as needed.
  3. The correlator.

The log file that receives the log statement output is the first log file that the correlator finds. If the correlator does not find a log file, the default is that the correlator sends the string and identifier to stdout.

Examples of using log statements

Suppose you insert DEBUG log statements without actions in a monitor. You specify ERROR as the log level for that monitor. The correlator ignores log statement output of log statements with identifiers of INFO or DEBUG. But then there are some problems. You use the engine_management correlator utility to change the log level to DEBUG. Now the correlator sends output from all log statements to the appropriate log file.

Following is another example:

log "Log statement number " + logNo() at DEBUG;
action logNo() {
   logNumber := logNumber + 1;
   return logNumber.toString();
}

In this example, the correlator always executes the log statement because it calls an action. However, the log level in effect must be DEBUG for the correlator to send the string to the log file. If the log level is anything else, the correlator discards the string because the log level in effect is lower than the log level in the log statement.

Strings in print and log statements

In both print and log statements, the string can be any one of the following:

  • Literal, for example: print "Hello";

  • Variable, for example:

    string welcomeMessage;
    ...
    log welcomeMessage;
    
  • Combination of both, for example:

    string welcomeMessage;
    ...
    print "Hello " + welcomeMessage + " Bye";
    

Internally, the correlator encodes all textual information as UTF-8. When the correlator outputs a string to a console or stdout because of a print statement, or sends a string to the log, the correlator translates the string from UTF-8 to the current machine’s (where the correlator is running) local character set. However, if you redirect stdout to a file, the correlator does not translate to the local character set. This ensures that the correlator preserves as much information as possible.

Sample financial application

This section describes a complete financial example, using the monitor techniques discussed earlier in this chapter.

This example enables users to register interest, for notification, when a given stock changes in price (positive and negative) by a specified percentage.

Users register their interest by generating an event, here termed Limit, of the following format:

Limit(userID, stockName, percentageChange)

For example:

Limit(1, "ACME", 5.0)

This specifies that a user (with the user ID 1) wants to be notified if ACME’s stock price changes by 5%. Any number of users can register their interests, many users can monitor the same stock (with different price change range), and a single user can monitor many stocks.

In EPL, the complete application is defined as:

event StockTick {
   string name;
   float price;
}

event Limit {
   integer userID;
   string name;
   float limit;
}

monitor SharePriceTracking {

   // store the user's specified attributes
   Limit limit;

   // store the initial price (this may be the opening price)
   StockTick initialPrice;

   // store the latest price – to give to the user
   StockTick latestPrice;

   // when a limit event is received spawn; creating a new
   // monitor instance for each user's request
   action onload() {
      on all Limit(*,*,>0.0):limit spawn setupNewLimitMonitor();
   }

   // If an identical request from a user is discovered
   // stop this monitor and die
   // If a StockTick event is received for the stock the
   // user specified, store the price and call setPrice
   action setupNewLimitMonitor() {
      on Limit(limit.userID, limit.name, *) die;
      on StockTick(limit.name, *):initialPrice setPrice();
   }

   // Search for StockTick events of the specified stock name
   // whose price is both greater and less than the value
   // specified – also converting the value to percentile format
   action setPrice() {
      on StockTick(limit.name, > initialPrice.price * (1.0 +
         (limit.limit/100.0))):latestPrice notifyUser();

      on StockTick(limit.name, < initialPrice.price * (1.0 -
         (limit.limit/100.0))):latestPrice notifyUser();
   }

   // display results to user
   action notifyUser() {
      log  "Limit alert. User=" +
         limit.userID.toString() +
         " Stock=" + limit.name +
         " Last Price=" + latestPrice.price.toString() +
         " Limit=" + limit.limit.toString();
      die;
   }
}

The important elements of this example lie in the life-cycle of different monitor states. Firstly a monitor instance is spawned on every incoming Limit event where the limit is greater than zero. Within setupNewLimitMonitor, the first on statement listens for other Limit events from the same user, upon detection of which the monitor instance is killed. This effectively ensures that there is a unique monitor instance per user per stock. This scheme also allows a user to send in a Limit event with a zero limit to indicate that they actually no longer want to monitor a particular stock. While this will not be caught by the original monitor instance’s event listener and will not cause spawning, it will trigger the event listener in the monitor instance of that user for that stock and cause it to die.

Then a single on statement (without anall) sets up an event listener to look for all StockTick events for that stock type for that user. Once a relevant StockTick is detected, new event listeners start seeking a specific price difference for that user. If such a price change is detected it is logged. Note that the log statement exploits data from variables used before and after the spawn statement (that is, limit and latestPrice, respectively).

This example also demonstrates how mathematical operations may be used within event expressions. Here, two on statements create event listeners that look for StockTicks with prices above and below the calculated price. The calculated price in this case is based on the initial price multiplied by the percentage specified by the user. The first event listener is looking for an increase in the share price to 105% of its original value, while the second is looking for a decrease to 95% of its original value.