12 Programación de scripts
Los scripts permiten añadir lógica propia allí donde el XML por sí solo no llega: convertir formatos, calcular valores derivados, reestructurar o filtrar la respuesta de un servicio y programar el diálogo de autenticación con un webservice. Este capítulo explica el entorno en el que se ejecutan los scripts y cómo se escriben para cada uno de esos usos.
12.1 Lenguajes disponibles#
| Lenguaje | Nombre en el XML | Intérprete |
|---|---|---|
| JavaScript | Javascript |
Duktape 2.5, integrado en Databbix. Implementa ECMAScript 5.1. |
| Python | Python |
CPython del sistema, integrado en Databbix. |
Todos los scripts de una colección de consultas (incluidos los de las autenticaciones) deben estar escritos en el mismo lenguaje.
12.1.1 JavaScript#
Escriba el código en ECMAScript 5.1:
varyfunction. Construcciones más modernas comolet,const, las funciones flecha o las plantillas de texto pueden no estar disponibles.Están disponibles los objetos estándar del lenguaje (
Math,Date,JSON,RegExp…), pero no hay acceso a ficheros, red, temporizadores ni módulos (require).Date.parse()solo reconoce con seguridad fechas en formato ISO 8601, por ejemplo:2026-09-29T10:15:00Z. Si la base de datos devuelve las fechas en otro formato, conviértalas en el SQL (to_char,extract(epoch…)) o analícelas a mano en el script.
12.1.2 Python#
Se usa la versión de Python instalada en el sistema, con su biblioteca estándar: se puede importar
re,datetime,json, etc.El código se ejecuta dentro del proceso de Databbix. Evite operaciones lentas o bloqueantes (red, disco): retrasan todo el monitor.
12.2 Dónde se escriben los scripts#
Un script es un elemento <script> con el atributo language. El código se escribe dentro de una sección CDATA para no tener que escapar los caracteres <, > y &:
<script language="Javascript"><![CDATA[
function kb2mb(valor) {
return Math.round(Number(valor) / 1024);
}
]]>
</script>Puede aparecer en tres lugares:
- En
<Queries>. -
Script global. Sus funciones están disponibles en todas las consultas de la colección.
- En
<query>. -
Script de consulta. Sus funciones solo están disponibles en esa consulta. Puede usar también las funciones globales.
- En
<authentication>. -
Script de autenticación. Sus funciones se usan en los elementos de esa autenticación.
Un script define funciones, que después se referencian por su nombre:
| Referencia | Uso |
|---|---|
Atributo transform de <item> |
Transformación de un valor (secciones 12.4 y 12.5). |
Elemento <transform> de <query> |
Transformación de la respuesta de un webservice (sección 12.6). |
Funciones de <authentication> |
Autenticación de webservice (sección 12.7). |
12.3 Entorno de ejecución#
12.3.1 Ámbito y estado#
Cada monitor tiene su propio intérprete. Dentro de él:
Los scripts globales se ejecutan una vez, al cargar la colección.
Cada consulta tiene su propio espacio de variables, que parte de una copia de las variables globales y al que se añaden los scripts de la consulta.
Ese espacio se conserva mientras el monitor está en marcha. Una variable global que una función modifique mantiene su valor en la siguiente ejecución de la misma consulta, pero no la ven las demás consultas.
Esto permite, por ejemplo, calcular una tasa a partir de dos lecturas consecutivas:
<query name="LocalCommitRate" module="PG">
<script language="Javascript"><![CDATA[
var anterior = null; // se conserva entre ejecuciones
function tasa(valor) {
var ahora = { t: Date.now() / 1000, v: Number(valor) };
var r = 0;
if (anterior !== null && ahora.t > anterior.t)
r = (ahora.v - anterior.v) / (ahora.t - anterior.t);
anterior = ahora;
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:tasa"/>
</query>Nota. El estado se pierde al reiniciar el servicio. Además, en Python la copia de las variables globales es superficial: una lista o diccionario creado en un script global es el mismo objeto en todas las consultas. Si necesita estado independiente, créelo dentro del script de la consulta.
12.3.2 Funciones de log#
Los scripts pueden escribir en el log del monitor con dos funciones, que aceptan cualquier número de argumentos:
| Función | Nivel de log |
|---|---|
log_debug(…) |
Depuración (se ve con LogLevel 7 o superior). |
log_info(…) |
Información (se ve con LogLevel 6 o superior). |
Los mensajes aparecen con el prefijo From Javascript: o From Python:.
<script language="Javascript"><![CDATA[
function estado(valor) {
log_debug("estado recibido:", valor);
return valor == "OPEN" ? 1 : 0;
}
]]>
</script>12.3.3 Tipos de datos#
Databbix pasa los datos a las funciones de dos formas:
Como texto: el valor de una columna o de una ruta JSON. Aunque la columna sea numérica, la función recibe una cadena; use
Number(valor)en JavaScript ofloat(valor)en Python para operar con él.Como objeto: las respuestas de los webservices, el JSON de un descubrimiento y las credenciales. Databbix decodifica el JSON antes de llamar a la función y codifica a JSON el objeto que devuelve. Si el texto recibido no es JSON válido, la función recibe la cadena tal cual.
12.3.4 Errores#
Si una función lanza una excepción, la operación falla y el mensaje de la excepción aparece en el log del monitor. El efecto depende del uso: en una transformación de ítem, el ítem pasa a no soportado; en la transformación de la respuesta de un webservice, todos los ítems de la consulta.
Para indicar a propósito que un dato no está disponible (y no registrarlo como error), la transformación de un webservice puede lanzar exactamente la cadena ZBX_NOTSUPPORTED:
throw "ZBX_NOTSUPPORTED"; // JavaScript
raise Exception("ZBX_NOTSUPPORTED") # PythonAtención. En JavaScript hay que lanzar la cadena, no un objeto Error: throw new Error("ZBX_NOTSUPPORTED") produce el texto Error: ZBX_NOTSUPPORTED, que Databbix no reconoce.
12.4 Transformación de valores en consultas SQL#
El atributo transform de un <item> aplica una función al valor de la columna antes de enviarlo a Zabbix. Tiene la forma Lenguaje:función:
<item name="local.ora.lastbackup.age" column="fin" transform="Javascript:edad"/>| Firma | función(valor) |
|---|---|
valor |
Valor de la columna, como texto. |
| Devuelve | El valor que se envía a Zabbix. Se convierte a texto: un número se envía tal cual, true/false como true/false (JavaScript) o True/False (Python). |
La función se llama una vez por ítem y ejecución. Si la consulta es FILTER, se llama para la fila que corresponde a la clave del ítem.
12.4.1 Ejemplo: fechas#
Segundos transcurridos desde una fecha y fecha en formato UNIX. Para que Date.parse funcione con cualquier base de datos, el SQL devuelve la fecha en formato ISO 8601:
<script language="Javascript"><![CDATA[
function edad(fecha) {
return Math.round((Date.now() - Date.parse(fecha)) / 1000);
}
function toUnix(fecha) {
return Math.round(Date.parse(fecha) / 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 fin
from v$backup_set
]]>
</sql>
<item name="local.ora.lastbackup" column="fin" transform="Javascript:toUnix"/>
<item name="local.ora.lastbackup.age" column="fin" transform="Javascript:edad"/>
</query>12.4.2 Ejemplo: traducir estados a números#
Zabbix trabaja mejor con valores numéricos para los disparadores. Una tabla de traducción evita escribir la lógica en el SQL de cada base de datos:
<script language="Javascript"><![CDATA[
var ESTADOS = { "OPEN": 0, "MOUNTED": 1, "STARTED": 2 };
function estadoInstancia(valor) {
var v = ESTADOS[String(valor).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:estadoInstancia"/>
</query>12.4.3 Ejemplo en Python#
La misma idea en Python, extrayendo un número de versión de un texto:
<script language="Python"><![CDATA[
import re
def version_mayor(valor):
m = re.search(r'(\d+)\.', valor)
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:version_mayor"/>
</query>Nota. En Python, la indentación es significativa: el código dentro de CDATA debe empezar en la primera columna, como en el ejemplo.
12.5 Transformación de descubrimientos#
En una consulta de descubrimiento, el transform del <item> recibe el JSON completo del descubrimiento como objeto y debe devolver un objeto con la misma estructura. Sirve para filtrar entidades o añadir macros calculadas.
| Firma | función(lld) |
|---|---|
lld |
Objeto { data: [ { "{#MACRO}": valor, ... }, ... ] }. |
| Devuelve | Objeto con la misma forma, que se envía a Zabbix. |
<query name="LocalTSDiscovery" module="DB" discovery="yes">
<script language="Javascript"><![CDATA[
function soloDatos(lld) {
var res = { data: [] };
for (var i = 0; i < lld.data.length; i++) {
var e = lld.data[i];
// descarta los tablespaces del sistema
if (/^(SYSTEM|SYSAUX)$/.test(e["{#TSNAME}"]))
continue;
// macro nueva: nombre en minúsculas
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:soloDatos"/>
<field name="TSNAME" column="tablespace_name"/>
</query>Nota. Filtrar en el SQL (where) es más eficiente. La transformación es útil cuando el criterio es difícil de expresar en SQL o cuando el mismo XML se usa con varias bases de datos.
12.6 Transformación de respuestas de webservice#
En las consultas REST, el elemento <transform> de la consulta procesa la respuesta del servicio antes de extraer los valores con las rutas (path) de los ítems:
<query name="..." module="..." type="REST" transport="GET" encode="Json">
<url>...</url>
<transform language="Javascript" function="miFuncion"/>
...
</query>| Firma | función(status_code, result) |
|---|---|
status_code |
Código HTTP de la respuesta (200, 404, 500…). En las consultas SOAP vale 0. |
result |
Cuerpo de la respuesta decodificado como JSON, o la cadena de texto si no es JSON. En SOAP, la respuesta convertida en JSON (sección 10.4.1). |
| Devuelve | Objeto sobre el que se aplican los path de los ítems y de los <field>. |
Atención. Sin <transform>, una respuesta con código distinto de 200 es un error. Con <transform>, la función recibe todas las respuestas y es la responsable de tratar los códigos de error.
La transformación se ejecuta una vez por llamada al servicio. Los usos típicos son tres: tratar errores, reestructurar la salida y filtrarla.
12.6.1 Tratar códigos de error#
El servicio de JD Edwards devuelve un error 500 con un código propio cuando el agente de la instancia está caído. La función lo convierte en un estado que Zabbix puede vigilar, y marca como no soportado lo que no tiene sentido medir:
<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 "Error HTTP " + 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 Reestructurar la salida#
A menudo, el dato que interesa en Zabbix no está directamente en la respuesta: hay que contarlo, sumarlo o calcularlo. La función construye un objeto nuevo con los valores ya calculados, y los ítems apuntan a él:
<query name="LocalJobSummary" module="API" type="REST" transport="GET" encode="Json">
<url>jobs</url>
<script language="Javascript"><![CDATA[
function resumen(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="resumen"/>
<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>La misma técnica sirve para respuestas que no son JSON: si el servicio devuelve texto, result es una cadena que la función puede analizar y convertir en un objeto.
12.6.3 Filtrar la salida#
En un descubrimiento REST, la función puede quedarse solo con los elementos que interesan. Aquí, de todas las instancias que devuelve el servicio, solo se descubren los servidores enterprise:
<query name="JDEDiscoverEnterprise" module="JDE" type="REST"
transport="GET" encode="Json" discovery="yes">
<url>instancesinfo</url>
<script language="Javascript"><![CDATA[
function filtraenterprise(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="filtraenterprise"/>
<item name="ts.jde.enterprise.discovery" path="result:[]"/>
<field name="INSTANCE" path="instanceName"/>
<field name="TYPE" path="targetType"/>
</query>12.6.4 Respuestas SOAP#
En las consultas SOAP la función recibe el JSON obtenido de la respuesta XML y status_code vale siempre 0. Hay que detectar los errores buscando un Fault en el propio JSON y tener en cuenta que un elemento repetido puede llegar como array o como valor único. La sección 10.4.2 muestra un ejemplo completo en JavaScript y Python.
12.6.5 Combinar con transformaciones de ítem#
Los ítems de una consulta REST también admiten el atributo transform, que se aplica después, al valor de cada ruta, igual que en las consultas SQL (sección 12.4). En los ítems de tipo INT, un resultado true/false se convierte en 1/0.
12.7 Programación de autenticaciones de webservice#
Un método de autenticación (sección 10.6) personaliza un motor de autenticación con funciones propias. Solo los motores HTTPAuth y RESTToken admiten personalización. Las funciones del método deciden cómo se obtienen las credenciales o el token y cómo se detecta que hay que volver a autenticarse.
12.7.1 Credenciales#
Todas las funciones que construyen credenciales reciben el mismo objeto, generado a partir del fichero de monitor:
[Webservice]
UserName = monitor
Password = secreto
Credentials = environment=JPD920
Credentials = role=*ALL{ "username": "monitor", "password": "secreto",
"environment": "JPD920", "role": "*ALL" }
UserName y Password se convierten en las claves username y password; cada línea Credentials añade una clave. De este modo, el XML describe el mecanismo y el .conf, los datos de cada instalación.
12.7.2 Verificación de llamadas#
Tras cada llamada al servicio, Databbix pregunta a la función de <verify-call> si la respuesta es válida:
| Firma | función(status_code, result) |
|---|---|
| Devuelve | Objeto con: call_ok (true si la llamada es válida), error (texto del error) y, solo en HTTPAuth, reauthenticate (true si hay que volver a autenticarse). |
Si call_ok es false, Databbix vuelve a autenticarse y repite la llamada una vez. En HTTPAuth, si además reauthenticate es false, la llamada falla directamente con el error indicado (credenciales incorrectas).
12.7.3 Métodos basados en el motor RESTToken#
Obtiene un token con una petición de login y lo envía en todas las llamadas siguientes.
<authentication type="RESTToken" name="nombre">
<script language="Javascript"> ...funciones... </script>
<login type="JsonPost">
<url>ruta/de/login</url>
<request-function language="Javascript" function="..."/>
<accept-function language="Javascript" function="..."/>
</login>
<token type="Header|Cookie" name="..."/>
<verify-call language="Javascript" function="..."/>
</authentication>| Elemento | Descripción |
|---|---|
<login> |
Petición de login. Se envía como POST a BaseURL + <url>, con cuerpo JSON. Use type="JsonPost". |
<request-function> |
función(credenciales). Devuelve el objeto que se envía como cuerpo de la petición de login. |
<accept-function> |
función(status_code, result). Analiza la respuesta del login y devuelve {authentication_ok, token, error}. |
<token> |
Dónde se envía el token en las llamadas: en una cabecera (type="Header") o en una cookie (type="Cookie"), con el nombre name. |
<verify-call> |
Verificación de cada llamada. Obligatorio. |
Ejemplo completo para el servidor AIS de JD Edwards:
<authentication type="RESTToken" name="AISAuth">
<script language="Javascript"><![CDATA[
function authGenerate(credentials) {
// cuerpo de la petición de login
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 errores = {
400: "Petición incorrecta",
403: "Credenciales no válidas",
415: "Content-Type incorrecto"
};
return { authentication_ok: false,
error: errores[status_code] || ("Error " + status_code) };
}
function verifyCall(status_code, result) {
// 444: token caducado -> volver a autenticarse
if (status_code == 403 || status_code == 444)
return { call_ok: false, error: "Token no válido" };
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>Nota. Si Webservice.AuthPersistFile está configurado, el token se guarda en ese fichero y se reutiliza tras un reinicio, sin pedir uno nuevo. El fichero contiene un secreto: protéjalo como el propio .conf.
12.7.4 Métodos basados en el motor HTTPAuth#
Usa los mecanismos de autenticación de HTTP. Sin <authentication>, toma las credenciales directamente del .conf (sección 3.15). Con <authentication>, admite:
| Elemento | Descripción |
|---|---|
<auth-generate> |
función(credenciales). Devuelve las credenciales efectivas: {method, username, password} con method BASIC, DIGEST o NTLM; o {method: "HEADER", header, value} para enviar una cabecera fija. |
<header> |
Cabecera adicional que se envía en todas las llamadas, con atributos name y value. El valor también puede ir como texto del elemento, y admite variables ${…}. |
<verify-call> |
Verificación de cada llamada. Opcional: si no existe, solo es válida una respuesta con código 200, y cualquier otro código se trata como un error de credenciales. |
Ejemplo: un servicio que se autentica con una clave de API en la cabecera X-API-Key. La clave se configura en el .conf, y el XML añade además una cabecera fija con la versión de la API:
<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: "Clave de API rechazada" };
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.empresa.com/v2/
Authenticator = ApiKeyAuth
Credentials = apikey=3f9a...c21