12 Script programming
Scripts let you add your own logic where the XML alone falls short: converting formats, computing derived values, restructuring or filtering a service response and programming the authentication dialogue with a web service. This chapter explains the environment scripts run in and how to write them for each of those uses.
12.1 Available languages#
| Language | Name in the XML | Interpreter |
|---|---|---|
| JavaScript | Javascript |
Duktape 2.5, embedded in Databbix. Implements ECMAScript 5.1. |
| Python | Python |
The system’s CPython, embedded in Databbix. |
All the scripts of a query collection (including those of authentication methods) must be written in the same language.
12.1.1 JavaScript#
Write the code in ECMAScript 5.1:
varandfunction. More modern constructs such aslet,const, arrow functions or template literals may not be available.The standard language objects are available (
Math,Date,JSON,RegExp…), but there is no access to files, network, timers or modules (require).The
Date.parse()function only reliably recognises ISO 8601 dates, for example:2026-09-29T10:15:00Z. If the database returns dates in another format, convert them in the SQL (to_char,extract(epoch…)) or parse them by hand in the script.
12.1.2 Python#
The Python version installed on the system is used, with its standard library:
re,datetime,json, etc. can be imported.The code runs inside the Databbix process. Avoid slow or blocking operations (network, disk): they delay the whole monitor.
12.2 Where scripts are written#
A script is a <script> element with the language attribute. The code is written inside a CDATA section so that the characters <, > and & need not be escaped:
<script language="Javascript"><![CDATA[
function kb2mb(value) {
return Math.round(Number(value) / 1024);
}
]]>
</script>It can appear in three places:
- In
<Queries>. -
Global script. Its functions are available to all the queries of the collection.
- In
<query>. -
Query script. Its functions are only available to that query. It can also use the global functions.
- In
<authentication>. -
Authentication script. Its functions are used by the elements of that authentication method.
A script defines functions, which are then referenced by name:
| Reference | Use |
|---|---|
transform attribute of <item> |
Transformation of a value (sections 12.4 and 12.5). |
<transform> element of <query> |
Transformation of a web service response (section 12.6). |
Functions of <authentication> |
Web service authentication (section 12.7). |
12.3 Execution environment#
12.3.1 Scope and state#
Each monitor has its own interpreter. Within it:
Global scripts run once, when the collection is loaded.
Each query has its own variable space, which starts as a copy of the global variables and to which the query’s scripts are added.
That space is kept while the monitor runs. A global variable modified by a function keeps its value in the next execution of the same query, but other queries do not see it.
This makes it possible, for example, to compute a rate from two consecutive readings:
<query name="LocalCommitRate" module="PG">
<script language="Javascript"><![CDATA[
var previous = null; // kept between executions
function rate(value) {
var now = { t: Date.now() / 1000, v: Number(value) };
var r = 0;
if (previous !== null && now.t > previous.t)
r = (now.v - previous.v) / (now.t - previous.t);
previous = now;
return r.toFixed(2);
}
]]>
</script>
<sql><![CDATA[
select sum(xact_commit) as commits from pg_stat_database ]]>
</sql>
<item name="local.pg.commitrate" column="commits" transform="Javascript:rate"/>
</query>Note. State is lost when the service restarts. Also, in Python the copy of the global variables is shallow: a list or dictionary created in a global script is the same object in all queries. If you need independent state, create it inside the query’s script.
12.3.2 Logging functions#
Scripts can write to the monitor’s log with two functions, which accept any number of arguments:
| Function | Log level |
|---|---|
log_debug(…) |
Debug (visible with LogLevel 7 or higher). |
log_info(…) |
Information (visible with LogLevel 6 or higher). |
Messages appear with the prefix From Javascript: or From Python:.
<script language="Javascript"><![CDATA[
function state(value) {
log_debug("state received:", value);
return value == "OPEN" ? 1 : 0;
}
]]>
</script>12.3.3 Data types#
Databbix passes data to functions in two ways:
As text: the value of a column or of a JSON path. Even if the column is numeric, the function receives a string; use
Number(value)in JavaScript orfloat(value)in Python to operate on it.As an object: web service responses, the JSON of a discovery and credentials. Databbix decodes the JSON before calling the function and encodes the returned object as JSON. If the text received is not valid JSON, the function receives the string as is.
12.3.4 Errors#
If a function throws an exception, the operation fails and the exception message appears in the monitor’s log. The effect depends on the use: in an item transformation, the item becomes not supported; in the transformation of a web service response, all the query’s items do.
To deliberately signal that a piece of data is not available (without logging it as an error), a web service transformation can throw exactly the string ZBX_NOTSUPPORTED:
throw "ZBX_NOTSUPPORTED"; // JavaScript
raise Exception("ZBX_NOTSUPPORTED") # PythonWarning. In JavaScript you must throw the string, not an Error object: throw new Error("ZBX_NOTSUPPORTED") produces the text Error: ZBX_NOTSUPPORTED, which Databbix does not recognise.
12.4 Transforming values in SQL queries#
The transform attribute of an <item> applies a function to the column value before sending it to Zabbix. It has the form Language:function:
<item name="local.ora.lastbackup.age" column="end_time" transform="Javascript:age"/>| Signature | function(value) |
|---|---|
value |
Column value, as text. |
| Returns | The value sent to Zabbix. It is converted to text: a number is sent as is, true/false as true/false (JavaScript) or True/False (Python). |
The function is called once per item and execution. If the query is a FILTER query, it is called for the row matching the item key.
12.4.1 Example: dates#
Seconds elapsed since a date, and a date as a UNIX timestamp. So that Date.parse works with any database, the SQL returns the date in ISO 8601 format:
<script language="Javascript"><![CDATA[
function age(date) {
return Math.round((Date.now() - Date.parse(date)) / 1000);
}
function toUnix(date) {
return Math.round(Date.parse(date) / 1000);
}
]]>
</script>
<query name="LocalLastBackup" module="DB">
<sql><![CDATA[
select to_char(max(completion_time) at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS"Z"') as end_time
from v$backup_set
]]>
</sql>
<item name="local.ora.lastbackup" column="end_time" transform="Javascript:toUnix"/>
<item name="local.ora.lastbackup.age" column="end_time" transform="Javascript:age"/>
</query>12.4.2 Example: mapping states to numbers#
Zabbix works better with numeric values for triggers. A mapping table avoids writing the logic in each database’s SQL:
<script language="Javascript"><![CDATA[
var STATES = { "OPEN": 0, "MOUNTED": 1, "STARTED": 2 };
function instanceState(value) {
var v = STATES[String(value).trim()];
return (v === undefined) ? 99 : v;
}
]]>
</script>
<query name="LocalInstanceStatus" module="Instance">
<sql><![CDATA[ select status from v$instance ]]></sql>
<item name="local.ora.instance.status" column="status"
transform="Javascript:instanceState"/>
</query>12.4.3 Python example#
The same idea in Python, extracting a version number from a text:
<script language="Python"><![CDATA[
import re
def major_version(value):
m = re.search(r'(\d+)\.', value)
return int(m.group(1)) if m else 0
]]>
</script>
<query name="LocalPGVersion" module="PG">
<sql><![CDATA[ select version() as v ]]></sql>
<item name="local.pg.version.major" column="v" transform="Python:major_version"/>
</query>Note. In Python, indentation is significant: the code inside CDATA must start in the first column, as in the example.
12.5 Transforming discoveries#
In a discovery query, the transform of the <item> receives the whole discovery JSON as an object and must return an object with the same structure. It is used to filter entities or add computed macros.
| Signature | function(lld) |
|---|---|
lld |
Object { data: [ { "{#MACRO}": value, ... }, ... ] }. |
| Returns | Object with the same shape, which is sent to Zabbix. |
<query name="LocalTSDiscovery" module="DB" discovery="yes">
<script language="Javascript"><![CDATA[
function dataOnly(lld) {
var res = { data: [] };
for (var i = 0; i < lld.data.length; i++) {
var e = lld.data[i];
// skip system tablespaces
if (/^(SYSTEM|SYSAUX)$/.test(e["{#TSNAME}"]))
continue;
// new macro: lower-case name
e["{#TSLOWER}"] = e["{#TSNAME}"].toLowerCase();
res.data.push(e);
}
return res;
}
]]>
</script>
<sql><![CDATA[
select tablespace_name from dba_tablespaces where status = 'ONLINE' ]]>
</sql>
<item name="local.ora.tspace.discovery" transform="Javascript:dataOnly"/>
<field name="TSNAME" column="tablespace_name"/>
</query>Note. Filtering in the SQL (where) is more efficient. The transformation is useful when the criterion is hard to express in SQL or when the same XML file is used with several databases.
12.6 Transforming web service responses#
In REST queries, the query’s <transform> element processes the service response before the values are extracted with the items’ paths (path):
<query name="..." module="..." type="REST" transport="GET" encode="Json">
<url>...</url>
<transform language="Javascript" function="myFunction"/>
...
</query>| Signature | function(status_code, result) |
|---|---|
status_code |
HTTP status of the response (200, 404, 500…). In SOAP queries it is 0. |
result |
Response body decoded as JSON, or the text string if it is not JSON. In SOAP, the response converted into JSON (section 10.4.1). |
| Returns | Object to which the path of the items and <field> elements is applied. |
Warning. Without <transform>, a response with a status other than 200 is an error. With <transform>, the function receives all responses and is responsible for handling error codes.
The transformation runs once per service call. Its typical uses are three: handling errors, restructuring the output and filtering it.
12.6.1 Handling error codes#
The JD Edwards service returns a 500 error with its own code when the instance agent is down. The function turns it into a state Zabbix can watch, and marks as not supported what makes no sense to measure:
<query name="JDEInstanceState" module="JDE" type="REST" transport="GET" encode="Json">
<url>instancestate</url>
<script language="Javascript"><![CDATA[
function instancestate(status_code, result) {
if (status_code == 200)
return result;
if (status_code == 500) {
switch (result.errorCode) {
case 13: return { instanceState: "AGENT_DOWN" };
case 14: throw "ZBX_NOTSUPPORTED";
}
}
throw "HTTP error " + status_code;
}
]]>
</script>
<transform language="Javascript" function="instancestate"/>
<parameter urlparameter="instanceName" order="1" type="STRING"/>
<item name="ts.jde.instance.state" path="instanceState" type="STRING"/>
</query>12.6.2 Restructuring the output#
Often the figure you want in Zabbix is not directly in the response: it has to be counted, summed or computed. The function builds a new object with the values already computed, and the items point to it:
<query name="LocalJobSummary" module="API" type="REST" transport="GET" encode="Json">
<url>jobs</url>
<script language="Javascript"><![CDATA[
function summary(status_code, result) {
if (status_code != 200)
throw "ZBX_NOTSUPPORTED";
var r = { total: 0, running: 0, failed: 0, maxDuration: 0 };
for (var i = 0; i < result.jobs.length; i++) {
var j = result.jobs[i];
r.total++;
if (j.state == "RUNNING") r.running++;
if (j.state == "FAILED") r.failed++;
if (j.duration > r.maxDuration) r.maxDuration = j.duration;
}
return r;
}
]]>
</script>
<transform language="Javascript" function="summary"/>
<item name="local.api.jobs.total" path="total" type="INT"/>
<item name="local.api.jobs.running" path="running" type="INT"/>
<item name="local.api.jobs.failed" path="failed" type="INT"/>
<item name="local.api.jobs.maxdur" path="maxDuration" type="INT"/>
</query>The same technique works for responses that are not JSON: if the service returns text, result is a string that the function can parse and turn into an object.
12.6.3 Filtering the output#
In a REST discovery, the function can keep only the elements of interest. Here, of all the instances returned by the service, only the enterprise servers are discovered:
<query name="JDEDiscoverEnterprise" module="JDE" type="REST"
transport="GET" encode="Json" discovery="yes">
<url>instancesinfo</url>
<script language="Javascript"><![CDATA[
function filterEnterprise(status_code, result) {
if (status_code != 200)
throw "ZBX_NOTSUPPORTED";
var res = { result: [] };
for (var i = 0; i < result.result.length; i++)
if (result.result[i].targetType == "entserver")
res.result.push(result.result[i]);
return res;
}
]]>
</script>
<transform language="Javascript" function="filterEnterprise"/>
<item name="ts.jde.enterprise.discovery" path="result:[]"/>
<field name="INSTANCE" path="instanceName"/>
<field name="TYPE" path="targetType"/>
</query>12.6.4 SOAP responses#
In SOAP queries the function receives the JSON obtained from the XML response and status_code is always 0. Errors must be detected by looking for a Fault in the JSON itself, bearing in mind that a repeated element may arrive as an array or as a single value. Section 10.4.2 shows a complete example in JavaScript and Python.
12.6.5 Combining with item transformations#
The items of a REST query also accept the transform attribute, which is applied afterwards to the value of each path, just as in SQL queries (section 12.4). In INT items, a true/false result is turned into 1/0.
12.7 Programming web service authentication methods#
An authentication method (section 10.6) customises an authentication engine with its own functions. Only the HTTPAuth and RESTToken engines can be customised. The method’s functions decide how the credentials or the token are obtained and how to detect that it is necessary to authenticate again.
12.7.1 Credentials#
All functions that build credentials receive the same object, generated from the monitor file:
[Webservice]
UserName = monitor
Password = secret
Credentials = environment=JPD920
Credentials = role=*ALL{ "username": "monitor", "password": "secret",
"environment": "JPD920", "role": "*ALL" }
UserName and Password become the keys username and password; each Credentials line adds a key. This way, the XML file describes the mechanism and the .conf file holds the data of each installation.
12.7.2 Call verification#
After each call to the service, Databbix asks the <verify-call> function whether the response is valid:
| Signature | function(status_code, result) |
|---|---|
| Returns | Object with: call_ok (true if the call is valid), error (error text) and, in HTTPAuth only, reauthenticate (true if it is necessary to authenticate again). |
If call_ok is false, Databbix authenticates again and repeats the call once. In HTTPAuth, if reauthenticate is also false, the call fails straight away with the given error (invalid credentials).
12.7.3 Methods based on the RESTToken engine#
The engine obtains a token with a login request and sends it in all subsequent calls.
<authentication type="RESTToken" name="name">
<script language="Javascript"> ...functions... </script>
<login type="JsonPost">
<url>login/path</url>
<request-function language="Javascript" function="..."/>
<accept-function language="Javascript" function="..."/>
</login>
<token type="Header|Cookie" name="..."/>
<verify-call language="Javascript" function="..."/>
</authentication>| Element | Description |
|---|---|
<login> |
Login request. Sent as a POST to BaseURL + <url>, with a JSON body. Use type="JsonPost". |
<request-function> |
function(credentials). Returns the object sent as the body of the login request. |
<accept-function> |
function(status_code, result). Analyses the login response and returns {authentication_ok, token, error}. |
<token> |
Where the token is sent in calls: in a header (type="Header") or in a cookie (type="Cookie"), with the name name. |
<verify-call> |
Verification of each call. Mandatory. |
Complete example for the JD Edwards AIS server:
<authentication type="RESTToken" name="AISAuth">
<script language="Javascript"><![CDATA[
function authGenerate(credentials) {
// body of the login request
return {
username: credentials.username,
password: credentials.password,
role: credentials.role,
environment: credentials.environment
};
}
function authAccept(status_code, result) {
if (status_code == 200)
return { authentication_ok: true,
token: result.userInfo.token };
var errors = {
400: "Bad request",
403: "Invalid credentials",
415: "Wrong Content-Type"
};
return { authentication_ok: false,
error: errors[status_code] || ("Error " + status_code) };
}
function verifyCall(status_code, result) {
// 444: token expired -> authenticate again
if (status_code == 403 || status_code == 444)
return { call_ok: false, error: "Invalid token" };
return { call_ok: true };
}
]]>
</script>
<login type="JsonPost">
<url>v2/tokenrequest</url>
<request-function language="Javascript" function="authGenerate"/>
<accept-function language="Javascript" function="authAccept"/>
</login>
<token type="Header" name="jde-AIS-Auth"/>
<verify-call language="Javascript" function="verifyCall"/>
</authentication>Note. If Webservice.AuthPersistFile is set, the token is saved in that file and reused after a restart, without requesting a new one. The file contains a secret: protect it like the .conf itself.
12.7.4 Methods based on the HTTPAuth engine#
The engine uses HTTP authentication mechanisms. Without <authentication>, it takes the credentials directly from the .conf (section 3.15). With <authentication>, it accepts:
| Element | Description |
|---|---|
<auth-generate> |
function(credentials). Returns the effective credentials: {method, username, password} with method BASIC, DIGEST or NTLM; or {method: "HEADER", header, value} to send a fixed header. |
<header> |
Additional header sent in all calls, with the name and value attributes. The value can also be given as the element’s text, and accepts ${…} variables. |
<verify-call> |
Verification of each call. Optional: if absent, only a response with status 200 is valid, and any other status is treated as a credentials error. |
Example: a service that authenticates with an API key in the X-API-Key header. The key is configured in the .conf, and the XML file also adds a fixed header with the API version:
<authentication type="HTTPAuth" name="ApiKeyAuth">
<script language="Javascript"><![CDATA[
function apiKey(credentials) {
return { method: "HEADER",
header: "X-API-Key",
value: credentials.apikey };
}
function verify(status_code, result) {
if (status_code == 401)
return { call_ok: false, reauthenticate: false,
error: "API key rejected" };
return { call_ok: true };
}
]]>
</script>
<auth-generate language="Javascript" function="apiKey"/>
<header name="Accept-Version" value="2"/>
<verify-call language="Javascript" function="verify"/>
</authentication>[Webservice]
BaseURL = https://api.example.com/v2/
Authenticator = ApiKeyAuth
Credentials = apikey=3f9a...c21