Actions
Actions are the tool for extracting arbitrary data from your application or the context. They are effectively lambda-like functions you can invoke from the entry and the exit phase of rules. They are defined by (A) specifying their input parameter and (B) giving a Java code snippet which defines how the result value is computed from these.
Defining Actions
Again, this is best explained by giving some simple examples extracted from the inspectIT Ocelot default configuration:
inspectit:
instrumentation:
actions:
#computes a nanosecond-timestamp as a long for the current point in time
'a_timing_nanos':
value: 'new Long(System.nanoTime())'
#computes the elapsed milliseconds as double since a given nanosecond-timestamp
'a_timing_elapsedMillis':
input:
#the timestamp captured via System.nanoTime() to compare against
'since_nanos': 'long'
value: 'new Double( (System.nanoTime() - sinceNanos) * 1E-6)'
'a_string_replace_all':
input:
'regex': 'String'
'replacement': 'String'
'string': 'String'
value: 'string.replaceAll(regex,replacement)'
'a_method_getClassFQN':
input:
_class: Class
value: '_class.getName()'
The code executed when an action is invoked is defined through the value
configuration property.
In YAML, this is simply a string. InspectIT however will interpret this string as a Java expression to evaluate. The result value of this expression will be used as result for the action invocation.
Note that the code will not be interpreted at runtime, but instead inspectIT Ocelot will compile the expression
to bytecode at runtime to ensure maximum efficiency.
As indicated by the manual primitive boxing performed for timestamp_nanos
the compiler has some restrictions.
For example Autoboxing is not supported!
However, actions are expected to return Object
instances, therefore manual boxing has to be performed.
Under the hood, inspectIT uses the javassist library, where all imposed restrictions can be found.
The most important ones are that neither Autoboxing, Generics, Anonymous Classes nor Lambda expressions are supported.
After actions have been compiled, they are placed in the same class loader as the class you instrument with them. This means that they can access any class that your application class could also access.
note
Exceptions within actions
Even if your action terminates with an exception or error, inspectIT will make sure that this does not affect your application. InspectIT will print information about the error and the faulting action. The execution of the action in the rule where the
failure occurred will be disabled until you update your instrumentation configuration.
Input Parameter
As previously mentioned actions are also free to define any kind of input parameter they need.
This is done using the input
configuration property.
This property maps the names of the input parameter to their expected Java type.
For example, the a_timing_elapsedMillis
action declares a single input variable named sinceNanos
which has the type long
. Note that for input parameter automatic primitive unboxing is supported.
Currently, array types like long[]
are not supported for input parameter.
Instead, you could use java.util.List
as input type.
Another example where the action even defines multiple inputs is a_string_replace_all
.
Guess what this action does? (Hint)
Special Parameter
The fourth example shown above is a_method_getClassFQN
, which uses the special input parameter _class
.
The fact that these variables are special is indicated by the leading underscore.
When normally invoking actions from rules, the user has to take care that all input parameter are assigned a value.
For special input parameter inspectIT automatically assigns the desired value.
This means that for example a_method_getClassFQN
can be called without manually assigning any parameter,
like it was done in the example of the rules section. An overview of all available special
input parameter is given below:
Parameter Name | Type | Description |
---|---|---|
_methodName | String | The name of the instrumented method within which this action is getting executed. |
_class | Class | The class declaring the instrumented method within which this action is getting executed. |
_parameterTypes | Class[] | The types of the parameter which the instrumented method declares for which the action is executed. |
_this | (depends on context) | The this-instance in the context of the instrumented method within which this action is getting executed. |
_args | Object[] | The arguments with which the instrumented method was called within which this action is getting executed. The arguments are boxed if necessary and packed into an array. |
_arg0,_arg1,...,_argN | (depends on context) | The N-th argument with which the instrumented method was called within which this action is getting executed. |
_returnValue | (depends on context) | The value returned by the instrumented method within which this action is getting executed. If the method terminated with an exception or the action is executed in the entry phase this is null . |
_thrown | Throwable | The exception thrown by the instrumented method within which this action is getting executed. If the method returned normally, the method is a constructor or the action is executed in the entry phase this is null . |
_context | InspectitContext | Gives direct read and write access to the current context. Can be used as data dictionary or to implement custom data propagation. |
_attachments | ObjectAttachments | Allows you to attach values to Java objects instead of to the control flow, as done via _context . This enables sharing data across multiple threads. |
_reflection | InspectitReflection | Allows you to access and cache fields and methods via reflection. |
_regex | InspectitRegex | Allows you to match and cache regex expressions. |
_agent | InspectitAgentInfo | Allows you to read agent information like the current version. |
Multiple Statements and Imports
Actions can easily become more complex, so that a single expression is not sufficient for expressing the functionality.
For this purpose we introduced the value-body
configuration property for actions as an alternative to value
.
value-body
allows you to specify a Java method body which returns the result of the action.
The body is given without surrounding curly braces. One example action from the default configuration making use of this is given below:
inspectit:
instrumentation:
actions:
'a_get_servlet_request_path':
imports:
- 'javax.servlet'
- 'javax.servlet.http'
input:
_arg0: ServletRequest
value-body: |
if(_arg0 instanceof HttpServletRequest) {
return java.net.URI.create(((HttpServletRequest)_arg0).getRequestURI()).getPath();
}
return null;
This action is designed to be applied on the Servlet API doFilter and
service methods.
Its purpose is to extract HTTP path, however in the servlet API it is not guaranteed that the ServletRequest
is a HttpServletRequest
.
For this reason the action performs an instance-of check only returning the HTTP path if it is available, otherwise null
.
Normally, all non java.lang.*
types have to be referred to using their fully qualified name, as done for java.net.URI
in the example above. However, just like in Java you can import packages using the import
config option.
In this example this allows us to refer to ServletRequest
and HttpServletRequest
without using the fully qualified name.
Caching
Repeatedly accessing the same data might create unnecessary performance overhead. Thus, actions allow to cache data
for general purpose. Such data can be read or overwritten within the same action. If multiple rules
use the same action, all of these rules will use the same cache. If an action has been changed, the cached data will be dropped.
The cache resembles an ordinary Java Map<Object, Object>
and is thread safe.
You can use the cache via the _cache
variable, which does not have to be included as input parameter.
The example below shows how to use the cache:
inspectit:
instrumentation:
actions:
# access the data, cache after first access
a_get_data:
input:
key: Object
value-body: |
Object data = _cache.get(key);
if (data == null) {
data = "Reading data...";
_cache.put(key, data);
}
return data;
# this action will use another cache
a_get_other_data:
# ...
Reflection
As mentioned above, in order to access and cache java.lang.reflect.Field
or java.lang.reflect.Method
instances,
you can use the _reflection
special parameter. Reflection helps you to access fields or invoke methods, which are normally
not accessible, because they are not visible for the instrumented method. Note that trying to access non-existing fields
or methods will throw an exception and disable the action until it has been updated.
If a Field
or Method
has been found, it will be cached globally. Thus, all actions use the same cached instances.
However, you can only use these instances via the _reflection
variable.
The method getFieldValue()
expects a class, an instance of such class and the field name and returns the field value.
The method invokeMethod()
expects a class, an instance of such class, the method name and the method arguments - if existing -
and returns the result of the invoked method.
The example below shows how to use the reflection variable:
inspectit:
instrumentation:
actions:
# access a field via reflection
a_get_field_value:
input:
_reflection: InspectitReflection
_class: Class
instance: Object
fieldName: String
value: _reflection.getFieldValue(_class, instance, fieldName)
# access a method via reflection
a_invoke_method:
input:
_reflection: InspectitReflection
_class: Class
instance: Object
methodName: String
firstArg: Object
secondArg: Object
value: _reflection.invokeMethod(_class, instance, methodName, firstArg, secondArg)
Regex
Since compiling regex expressions is relatively expensive, it makes sense to compile them once and cache the result.
When using the _regex
special parameter, all compiled results will be cached globally.
Thus, all actions use the same cached regex patterns.
However, you can only use these patterns via the _regex
variable.
The method pattern()
compiles a single regex expression and returns the pattern.
The method matcher()
expects a regex expression and some input string and returns the matcher.
The method matches()
expects a regex expression and some input string and returns a boolean.
The example below shows how to use regex variable:
inspectit:
instrumentation:
actions:
a_input_matches:
input:
_regex: InspectitRegex
regex: String
input: String
value: Boolean.valueOf(_regex.matches(regex, input))
Default Actions
InspectIT Ocelot provides a large set of default actions, which can be used within any configuration. Such actions allow you for example to assign values to data keys, print debug information or replace strings via regex. You can find the full set of default actions here.
Below, some important actions are explained in more detail. The examples show how to apply the actions within rules. In the upcoming section you will find more detailed information about Invoking Actions.
Assigning Values
Data keys can only be used withing rules (e.g. for metric tags or tracing attributes), if they have an assigned value.
You can assign values via any action. If you would like to assign a constant value or copy another value to a data key,
you can use the actions a_assign_value
, a_assign_null
, a_assign_true
or a_assign_false
.
rules:
r_rule:
entry:
some_value:
action: a_any_action
copied_value:
action: a_assign_value
data-input:
value: some_value
constant_value:
action: a_assign_value
constant-input:
value: "constant"
is_entry:
action: a_assign_true
Attaching Values
Object attachments allow you to store and access data outside the current control flow.
The inspectIT agent provides one global dictionary, where any Java object can serve as a key,
which points to another dictionary of data keys and values.
In Java, this would be resembled by a Map<Object, Map<String, Object>>
.
For instance, you would like to use recorded data within two separate threads.
If these threads are accessing the same Java object, you can attach this object to the global dictionary
together with its data key and value in the first thread. After that, you can read the data in the second thread
via the attached Java object.
For this, we provide the default actions a_attachment_get
, a_attachment_put
and a_attachment_remove
.
rules:
r_first: # first thread
entry:
some_value: # record value
action: a_any_action
shared_object: # somehow access shared object
action: a_assign_value
data-input:
value: _arg0
attach:
action: a_attachment_put
data-input:
target: shared_object
value: some_value
constant-input:
key: 'my-key'
r_second: # second thread
entry:
shared_object: # somehow access shared object
action: a_assign_value
data-input:
value: _arg0
some_value: # get previously recorded value
action: a_attachment_get
data-input:
target: shared_object
constant-input:
key: 'my-key
Debugging
We provide two debug actions, which might help you with understanding the data flow of your instrumentation:
a_debug_println
and a_debug_println_2
. These will print the provided values at the standard system output.
rules:
r_rule:
entry:
some_value:
action: a_any_action
another_value:
action: a_another_action
debug:
action: a_debug_println
data-input:
value: some_value
debug_2:
action: a_debug_println_2
data-input:
a: some_value
b: another_value
Agent Information
Under certain circumstances, you may not want to execute certain actions, or you may only want to execute them
if a certain agent version is used. For example, if a used function is only available from a certain version.
Thus, we provide some actions to read or compare the agent version: a_agent_version
and a_agent_isAtLeast
rules:
r_rule:
entry:
version:
action: a_agent_version
isAvailable:
action: a_agent_isAtLeast
constant-input:
version: "2.0.0"
Logical Operators
We also provide some logical operators for checking specific conditions of data values:
a_logic_isNull
, a_logic_isNotNull
, a_logic_and
, a_logic_or
and a_logic_isTrueOrNotNull
.
rules:
r_rule:
entry:
isEntry: # true
action: a_assign_true
isFalse: # false because isEntry is not null
action: a_logic_isNull
data-input:
value: isEntry
isTrue: # true because isExit is null
action: a_logic_isNull
data-input:
value: isExit # data value not set
isAlsoFalse: # false because only one is true
action: a_logic_and
data-input:
a: isFalse
b: isTrue
Replacing Strings via Regex
If you would like to sanitize some strings, for instance to remove sensitive data, you can use the following default
actions: a_regex_replaceAll
, a_regex_replaceAll_multi
, a_regex_replaceMatch
, a_regex_replaceMatch_multi
,
a_regex_replaceFirst
, a_regex_extractFirst
, a_regex_extractFirst_multi
, a_regex_extractMatch
and a_regex_extractMatch_multi
.
rules:
r_rule:
entry:
http_path_raw:
action: a_any_action
http_path: # sanitize http path
action: a_regex_replaceMatch
data-input:
string: http_path_raw
constant-input:
'pattern': '\/apps\/.+\/.+'
'replacement': '/apps/{service}/{location}'