Skip to main content

Misc functions

customFunction

Commentary

added in

Invokes custom functions defined in either Python, Ruby, or JavaScript.

For the most part, ShadowTraffic's core library is sufficient to generate a wide range of data. But sometimes, the existing functions are going to be missing something that only you can implement. customFunction is the escape hatch to let you generate any kind of data you want.

ShadowTraffic natively executes code for each language using GraalVM 23+. This is important to know because the Graal implementation of each language may have slight difference to other platforms. For reference:

All custom functions must be defined with a single parameter, which is a map. This map gives you function useful context about its invocation, such as:

  1. args: a map of name -> values passed as arguments to the function 1
  2. call-id: a unique ID for this function
  3. call-state: a map of ID -> state for all functions in this evaluation tree 2
  4. vars: a map of variable name -> value for any defined variables

Further, all functions must return a map with at least the key value, which indicates the value that has been created. It may additionally return a key state which returns an updated view of `call-state.


Examples

Defining a formula

The easiest example to demonstrate is a scalar function. Imagine you have some Python that looks like this:

factor = 387.34575

def formula(obj):
return {
"value": factor * (obj["args"]["x"] + obj["args"]["y"]),
}

To execute this code, mount it into the container at, say, path /home/python/basic_formula.py.

Then invoke customFunction, specifying the language, the file where the code can be found, and the function name.

Optionally, you can pass named arguments in with args. ShadowTraffic will evaluate any functions within and pass their make their results available to the custom function.

{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/basic_formula.py",
"function": "my_formula",
"args": {
"x": {
"_gen": "normalDistribution",
"mean": 100,
"sd": 10
},
"y": {
"_gen": "uniformDistribution",
"bounds": [
50,
75
]
}
}
}

Carrying state

Some functions are only useful if they can remember prior values that they generated. To do this, ShadowTraffic has an internal state mechanism that's available to use in customFunction.

Here's a simple Python function that generates a series of alternating boolean values: false, true, false, true, and so on.

def stateful_function(obj):
call_id = obj.get("call-id")
state = obj.get("call-state") or {call_id: False}
v = state.get(call_id)

return {
"value": v,
"state": {
call_id: not(v)
}
}

The Python code first obtains the call-id of this function. It then grabs the state for this function, or initializes a value if it doesn't yet exist. Finally, it returns an updated call state by negating the previous boolean value.

Invoke the code like so:

{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/stateful_function.py",
"function": "my_stateful_function"
}

Writing sequentialInteger

To better understand how custom functions with state work, we can implement an existing function - sequentialInteger - from scratch.

In the same manner as the previous example, grab the call-id of this function and initialize its state to 0 if it doesn't yet have a value.

Then return that value, plus an updated view of the state by incrementing the value by 1.

def seq_int(obj):
call_id = obj.get("call-id")
state = obj.get("call-state") or {call_id: 0}
v = state.get(call_id)

return {
"value": v,
"state": {
call_id: v + 1
}
}

Finally, invoke the function inside ShadowTraffic.

{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/seq_int.py",
"function": "sequential_integer"
}

Specification

JSON schema

{
"type": "object",
"properties": {
"language": {
"type": "string",
"enum": [
"python",
"ruby",
"js"
]
},
"file": {
"type": "string"
},
"function": {
"type": "string"
}
},
"required": [
"language",
"file",
"function"
]
}