1. Arithmetic, boolean and bitwise expressions
Please see below for a brief overview of the Entuity statement language's core functionality.
1. Arithmetic, boolean and bitwise expressions:
Support for the standard arithmetic operators +, -, *, /, logical operators &&, ||, !, comparison operators <, >, <=, >=, ==, != and bitwise operators &, |, ^, ~.
e.g.
1 + 60 * 60
now() < 100000 || now() >= 110000
2. 'this' context:
The current context can be referred to by using the keyword this. In many cases, explicit use of this is not necessary because it will be implicitly assumed when e.g. an attribute name is referenced.
The initial top-level this context is always a StormWorks object, and is usually the StormWorks root object.
It is also possible, however, for the this context and the var context (see 3. 'var' context below) to be of types that are not a StormWorks object, via the use of e.g. foreach or eval. For example, they may be a string, a uint32, a list, or any other supported type.
e.g.
this.name /* the same as just name by itself */
doFunkyStuff(this)
/* call imaginary function doFunkyStuff and pass it the current object as a parameter */
foreach([1, 2, 3], this * 3)
/* sets the this context to each integer in the list in turn then evaluates the second expression in that context. Returned value will be the list of integers (3, 6, 9) */
3. 'var' context:
In addition to the current object context in which all statements are evaluated, it is also possible to specify one extra object to be passed into the statement parser. To do this:
- the object's type must be passed as a second parameter to the TypeContext that is given to the prepare() method for the statement.
- an actual instance of this type must be passed as a second parameter to the ObjectContext that is given to the evaluate() method of the statement object.
- this second object can then be referenced using the keyword var, which works exactly like the keyword this, except that its presence is always mandatory and never implied (in order to avoid ambiguities).
Just like the this context, it is possible for the var context to be any supported data type, such as a string or list.
e.g.:
var.name /* get the name attribute of the var object */
var.ref.device /* get the device associated with the (presumably port type) var object */
this == var /* test if the two available objects are in fact the same */
4. Attribute success:
You can access a particular attribute's value on an object using the dot-operator in the standard C-style syntax.
e.g.
this.name
ref.device.ref.ports
5. Casting:
In the statement language, upcasting and downcasting of object types is supported with a funtion-style casting syntax. Casting to a base type should always be successful, whereas casting to a derived type may cause a runtime error if the actual object turns out not to be of that type.
Casting is particularly useful when dealing with objects returned from a general function that you know to be of a more specific type than the function's signature specifies.
e.g.
foreach(all_of_type_no_view("port"), port(this).ifDescr)
/* use the fact that we know all items in the list are actually ports to access their ifDescr attribute */
6. Comments:
C-style comments delimited by /* and */ are supported.
e.g.
1 + /* surely I can come up with a better example than this?? */ 2
7. String literals:
String literals work as expected, with adjacent string literals being concatenated into a single string as in C. They can be indexed to get the character at a specific position.
e.g.
"a string literal"
"a " "string " "literal"[3]
["a", "list", "of", "string", "literals"]
8. List literals:
You can specify list literals by using square brackets and a list of elements separated by commas. All elements are expected to be of the same type (e.g. object, string, number, etc) and the compiler will attempt to enforce this.
e.g.
[ 1, 2, 3, 4, 5 ]
[ "one", "two", ftime(now()) ]
[ [ 1.2, 2.3 ], [ 3.4, 4.5, 5.6 ], [ 6.7 ] ]
It is important that all list elements are the same type because lists are generally processed by repeating them and applying the same function or expression to each element using e.g. a foreach expression. If the elements are of differing types, then it may not be possible to apply the same operation to them. If the list element type is not known at compile time, it will not be possible to use foreach on it.
9. Struct literals:
You can specify struct literals using curly brackets and an arbitrary list of elements separated by commas. Elements can be of any type, and the resulting structure can be subscripted array-style in order to obtain a particular element.
e.g.
{ 23, "some string", [ "a", "list" ], now() }
{ "three", "string", "values" }[0] /* returns value of first element, i.e. "three" */
The difference between a struct and a list is that in a struct the elements can be of differing types, and therefore you cannot use foreach to iterate over the contents of a struct.
10. Subscripting:
Certain types support subscripting by index using C-style array access. The types that support this are strings, structs and lists. Note, in order to be able to determinte the return type at compilation, subscripts to structs must be a constant value. Only constant integers are allowed, not expressions (even if these do evaluate to a constant).
Note, not all lists are internally represented as vectors. If you attempt to subscript a list that is, at compile time, represented as a non-subscriptable type (e.g. a set), you will get a compile-time error. You can force subscripting to work by casting the list to a generic list type before subscripting it, but this will mean that, at runtime, the subscripting will be performed by an inefficient iteration over the contents of the set.
The negative index will subscript backwards from the end of the container, i.e. -1 will be the last item, -2 the penultimate item, etc.
e.g.
"onetwothree"[4] /* returns the fifth character, w */
{ 4, "whatever", 3.14 }[2] /* returns the 3rd element, 3.14 */
"testing123"[now()] /* valid, but probably throw range-check error at runtime */
{ "not okay example", 23, 42, 7 }[1 + 2] /* will produce compile-time error */
ref.devices[1] /* get the second device in the list */
unique(ref.devices)[1] /* will produce compile time error since unique returns a set */
11. Sequence operator:
You can evaluate a sequence of expressions by separating them with the sequence operator, which is the semicolon ; . This works in the same way as the comma operator in C, with the value returned being that of the last expression in the sequence (in this case, the semicolon is used instead of the colon to avoid ambiguities when parsing (e.g. foreach expressions)).
Note, because the return values of all expressions except the last one are thrown away, this operation is most useful when used with other operations that have side effects. In particular, variable declarations can be made and then referred to later on.
e.g.
1 + 2; 3 + 4; 5 + 6 /* returns 11 */
logMessage("about to do snmp call...\n");
snmp(ref.device.name, ref.device.snmpCommunity, snmpv1,
makeoid(".1.3.6.1.2.1.2.2.1.16", portIndex.ifIndex))
12. Function calls:
You can make calls to loadable functions that implement the pure virtual class FunctionInterface, which allows a simple mechanism for language extensions.
- functions can and should specify a function signature, which allows the statement language (at compile time) to have type information about the return type and/or parameter types for functions.
- some functions, e.g. all_of_type_no_view() have a return type that is undetermined until runtime, because it depends on the value of their input parameters. If possible, a common base type should be specified, therefore allowing the user to cast the returned values if they know their full type.
e.g.
avg(stream.shortUtilization.inUtil.samples(6))
/* returns the mean value of the last six inUtil samples */
foreach(all_of_type_no_view("device"), device(this).name)
/* uses the all_of_type_no_view function to get a list of all device names
*/
13. isa:
The isa operator has method-call syntax. If the object it is called for is of the type specified, or is derived from that type, the isa operator returns true.
e.g.
this.isa(device) /* true if current object is a device */
foreach(ref.devices, foreach(ref.ports, { ifDescr, this.isa(frPort) }))
14. Streams:
You can access stream instances that are associated with an object via method-call syntax, using the keyword stream and the name of the stream you wish to access. You can qualify the data returned by providing the name of a valid stream attribute, in which case only this attribute is returned. There are also several built-in methods available on streams for getting a number of samples or the samples within a specified timespan.
The samples method takes a single positive integer argument specifying the number of samples to return, rather than the entire stream.
The timespan method takes either one or two integer arguments. A single argument specifies a timestamp from which time (if positive) onward to take samples, or from which time (if negative) in the past to take samples. If two arguments are supplied, then all samples between these two timestamps are returned.
e.g.
stream.shortUtilization
/* the entire shortUtilization stream for the current (port) object */
stream.shortUtilization.inUtil
/* all the inUtil data for the current object */
stream.shortUtilization.inUtil.samples(2)
/* the last two inUtil samples */
stream.shortUtilization.inUtil.timespan(-600)
/* all inUtil samples in the last ten minutes */
stream.shortUtilization.timespan(100000)
/* all data since timestamp 100000 */
stream.shortUtilization.timespan(100000, 200000)
/* all data between timestamp 100000 and 200000*/
foreach(all_of_type_no_view("port"), { port(this).ifDescr,
port(this).stream.shortUtilization.timespan(-300) })
/* a list of structs containing the interface description and last five minutes shortUtilization samples for every port on the system */
There are also several options to control whether samples to either side of the start and end times are included:
stream.portTrafficRateRawData.timespan(-3600).unclipped
/* for historical reasons this is the default, and means that samples whose timestamp is within half a delta of the end
timestamp may be included */
stream.portTrafficRateRawData.timespan(-3600).clipped
/* only include samples whose timestamp is strictly inside the requested period */
stream.portTrafficRateRawData.timespan(-3600).shifted
/* effectively shift both the start and end times forward by half a delta. This can be appropriate when the data stored
in the sample covers a period of time prior to the sample time */
Note the following caveats:
- when there are not enough samples to fulfill a samples() call, a Short Stream Exception is thrown.
- if a stream request returns an empty samples list, a Short Stream Exception is thrown.
- virtual streams do not always implement the samples() method correctly, so may return an incorrect number of samples.
- when you call samples() on a chain stream, the call is delegated to the first valid component stream. This means that normally the samples will be returned from the most fine-grained/frequently-polled component stream. It also means that if there are not enough samples in that stream, a Short Stream Exception will get thrown.
15. Associations (ref):
You can obtain any object or objects associated with an object, by using the ref keyword and the name of the appropriate association attribute. The return value will either be a single object or a list of objects depending on how many associations there are.
e.g.
ref.device /* obtain the device associated with the current (port) object */
ref.ports /* obtain a list of all the ports associated with the current (device) object */
foreach(ref.reportDefinitions, foreach(ref.scheduledReports, { name, timeInterval }))
/* from the root object get a list of all the scheduled reports, giving their name and periodicity in seconds */
In some cases, an association is defined as returning multiple objects, but in practice only ever has one. In this case, you can use head() (see below) to get the first item of the returned list.
16. eval:
The eval operator takes a new this context, a new var context and an expression as arguments, and evaluates the expression within these new contexts. It provides a simple mechanism for temporarily changing one or both of the context objects.
e.g.
eval(this, ref.device, var.name)
/* changes the var context to be the associated device, then returns its name */
eval(head(ref.ports),
head(tail(ref.ports)),
foreach(ref.IpAddresses,
this,
foreach(var.IpAddresses,
true,
this == context[1])
)
)
/* makes the 'this' context the first associated port, and the 'var' context the second associated port. It then returns a list of IP addresses that they share */
17. if:
The if operator takes three expressions as arguments. It evaluates the first expression, and if the value returned is true (non-null) then it returns the result of the evaluating the second, otherwise it returns the result of evaluating the third. It therefore behaves the same as the ternary if operator '?' in C. Note, both the second and third expressions should have the same return type:
e.g.
if (id != 1, "non-root object", "the root object!")
/* returns string saying if the current object is root or not */
if (ref.devices, head(ref.devices).ref.ports, null)
/* if there are any associated devices returns a list of the ports on the first one, otherwise returns null */
18. case statement:
You can specify case statements to compare the result of a given expression against multiple expressions (which may simply be constants). When a match is found, a corresponding expression is evaluated and the result is returned. If no match is found, then the expression associated with the mandatory default case is evaluated and returned.
e.g.
case (rand(6))
{
0: "zero beans",
1: "one bean",
2: "two beans",
default: "lots of beans"
}
The context in which each test case is evaluated is the same as the context of the whole case statement. This means you can do more complex tests using an arbitrary list of expressions that either return true or false.
e.g.
foreach(ref.devices,
concat(name,
case (true)
{
try(in(topo_state_category(ref.deviceTopoNode.topoNodeState), [0, 1, 2, 3, 4]), false): " has valid node state",
ref.deviceTopoNode != null: " has topo node with unexpected value",
(now() - head(foreach(dump_object(), this[1], this[0] == "Create time")) < 15 * 60): " has no topo node, but was created less than 15mins ago",
default: " has no topo node and is more than 15mins old!"
}
))
19. head:
You can use the head operator to return the first item in the given list, or NULL if the list empty. This can be useful when you have a list that you know to contain only one item.
e.g.
head([1, 2, 3]) /* returns 1 */
head([]) /* returns NULL */
head(ref.devices) /* returns the first device in the list of devices */
20. tail:
You can use the tail operator to remove away the first item in a list, and then return the remaining list. If the list is empty, then an empty list is returned.
e.g.
tail([1, 2, 3]) /* return [2, 3] */
tail([]) /* return [] */
tail(ref.devices) /* returns a list of all except for the first device */
21. flatten:
You can flatten a list using the flatten operator. This means that any elements that are themselves lists are then replaced by their own elements. This is done recursively, so that the resulting list will not contain any elements that are lists.
e.g.
flatten([ [1,2], [3,4,5], [6,7,8,9] ])
/* returns [1,2,3,4,5,6,7,8,9] */
flatten(foreach(ref.reportDefinitions, foreach(ref.scheduledReports, name))
/* returns the names of all the scheduled reports in a single, flat list */
22. unique:
You can use the unique operator to return a copy of a list with all duplicates removed.
This will only work for lists whose items support the < operator, which lists themselves do not (i.e., you cannot use the unique operator on lists of lists (but you can use the flatten operator)).
Note, the 'list' returned is a set, which means that the items are also in ascending order. Sets behave in the same way as lists in nearly all ways, but they do not support subscripting. Sets do support a more efficient lookup when using the in function, so unique can be used to improve the performance of this function if used repeatedly on the same list.
e.g.
unique([5, 2, 3, 2, 2, 3, 4, 1])
/* returns [1,2,3,4,5] */
unique(flatten(foreach(all_of_type_no_view("DeviceEx"), DeviceEx(this).ref.ports)))
/* returns a list of all the unique ports */
23. foreach:
The foreach expression takes two expression parameters, and there is an optional third parameter:
- the first expression returns a list of items whose type is an appropriate context for evaluating the second expression.
- the return value is the list of values obtained by evaluating the second expression for each item in the list that is returned by the first expression.
- The optional third expression is used as a filter to determine whether or not to process each item - if it returns false when evaluated in the context of the current item in the list, then it is skipped.
Whilst within a foreach loop, you can access the current index using the keyword position.
e.g.
foreach(ref.devices, name)
/* returns a list of all the device names (assuming current object is root object) */
foreach(all_of_type_no_view("port"), foreach(port(this).ref.device, { name, devType }))
/* returns a list of structs, each of which contains the name and devType for a device. Each device will be listed once for every port that it has */
foreach(all_of_type_no_view("device"), device(this).name, device(this).devType == 168)
/* returns a list of the names of all routers */
head(foreach([1,2,3,4,5], this, position == 2))
/* return the value of the element at position 2, i.e. 3 */
24. context:
Each time you enter a new foreach loop, a new object context is pushed onto a context stack, which temporarily hides the previous this context (the var context remains the same). Likewise, when an eval is used, both the this and var contexts can be modified.
However, you can still access contexts other than the current one by using the context keyword, followed by a subscript. The context operator returns a struct that contains all the this contexts that have been pushed onto the stack, with the current context being index zero, the previous being index one, the next up being index three, etc.
e.g.
foreach( ref.devices, context[1].id )
/* outputs a list containing the root objects id a number of times equal to the number of devices associated with it */
foreach(
foreach(all_of_type_no_view("DeviceEx"), DeviceEx(this)),
foreach(
ref.ports,
foreach(
ref.IpAddresses,
context[1].ifDescr,
foreach( /* if an empty list then filter fails */
context[3].ref.devices,
foreach(
DeviceEx(this).ref.ports,
foreach(
ref.IpAddresses,
1,
ipAddr == context[3].ipAddr && ref.port != context[3].ref.port
)
)
)
)
)
)
/* return a list of the ifDescr attributes for each port that shares an ip address with at least one other port */
25. variable:
You can use the keyword variable to declare a local variable and initialise it to a value. It is not possible to assign to variables, which means their value cannot be changed, and it is compulsory to give them a value at declaration time. The variable's type is given by the return type of the expression that is used for initialising the variable.
Within a certain scope, you can access all variables declared in the scope, or any enclosing scope. However, you can hide a variable in an enclosing scope by declaring a variable of the same name in the current scope.
e.g.
variable sentence = ["my", "name", "is", "Michael", "Caine"];
variable word = head(sentence);
word
/* return value will be the string "my" */
variable x = 7;
foreach([1, 2, 3], variable x = x * this; x)
/* return value will be the list of integers (7, 14, 21) */
26. sort:
You can use the sort expression to sort a list, and return a new list in the sort order. It takes three parameters:
- the first is the list of items to be sorted.
- the second is the expression by which they are to be sorted.
- the third is the sort order - ascending (asc) or descending (desc).
e.g.
sort([1,4,3,9,11,23,5,23], this, asc)
/* return value will be the list (1, 3, 4, 5, 9, 11, 23, 23) */
foreach(sort(ref.devices, count(ref.ports), desc), {name, count(ref.ports)})
/* return value will be a list of structs containing the name of each device and a count of its number of ports, sorted in descending order of that count */
27. top:
The top operator takes a list and an integer N, and returns the first N items from the list. If there are insufficient items in the list, then as many items as are available will be returned.
e.g.
top(sort(ref.devices, name, desc), 5)
/* return the first 5 devices alphabetically by name */
28. bottom:
The bottom operator takes a list and an integer N, and returns the last N items from the list. If there are insufficient items in the list, then as many items as are available will be returned.
e.g.
bottom([1,2,3,4,5], 2)
/* returns the list (4, 5) */
29: try:
The keyword try is used to execute an expression that might fail due to a thrown exception where you don't want a thrown exception to cause the termination of the entire statement.
- the first argument is the expression to be evaluated.
- the second argument is an expression to evaluate if an exception should be thrown. The context for this second argument will be a struct consisting of the original context, and a string holding the what() message of the exception that was caught. This will enable you to e.g. log this message if need be.
- It is important to note that both the first and second arguments must still return values which are of the same type, otherwise the statement will fail at compile time.
e.g.
/* don't want to fail if something goes wrong with one item in list */
foreach(find(port, "simple;1", 1), try(portEx(this).ifAlias, logMessage("exception caught: " + this[1]); "<unknown>"))
A try block in the statement language catches all exceptions (so long as they are derived from std::exception), except for the special Entuity exceptions NonPollableDeviceException and FunctionBarredException, which will get propagated up regardless.
30. timeseries:
The timeseries function allows you to select samples from one or more streams on the same object, using expressions to determine the resulting the resulting merged sample values. It takes a variable number of arguments, depending on the number of attributes in the returned stream:
- the first and second arguments are the start time and end time, which are unix timestamps. These determine the time frame of the returned samples.
- the third argument is the sample interval in seconds. This determines the output sample frequency that you want the resulting stream samples to be set to. This may result in values being padded to create the necessary entries. If you use a value of zero here, the original timestamps will be maintained, but in this case you can only use input samples from a single stream.
- the fourth argument is a prime time definition string, with the empty string representing no prime time is to be applied.
- one or more stream attribute definitions should then follow. Each of these is an expression that usually references an attribute on a stream using the special sAttr symbol (otherwise it will be a constant value). It is recommended to surround each stream attribute expression in brackets to help the parser.
e.g.
timeseries(now() - 3600, now(), 0, "",
sAttr.v_unifiedDeviceCPU.v_cpuUtilPercent)
timeseries(now() - 3600, now(), 300, "",
(sAttr.routerSystemResources.freeIoMem) +
(sAttr.routerSystemResources.freeProcMem))
timeseries(now() - 3600, now(), 300, "09:00 17:00 1 5",
(sAttr.v_unifiedDeviceCPU.v_cpuUtilPercent) *
(sAttr.routerSystemResources.freeProcMem) / 100.0)
Comments
0 comments
Please sign in to leave a comment.