ClusterControl Domain Specific Language (CCDSL)
CCDSL is the language used to write ClusterControl advisors, programs, and widgets, small scripts that run inside the ClusterControl cmon controller process and are packaged into tarballs (such as s9s.tar.gz and common.tar.gz) that cmon loads on first start, or after an upgrade.
The syntax is JavaScript-flavored, with extensions for accessing ClusterControl's internal data structures and functions, for example, executing SQL statements, running shell commands across cluster hosts, reading historical monitoring data, editing configuration files, raising alarms, enqueueing controller jobs, and rendering graphs. The results are then processed into advisors, alerts, or other actions.
1. Language basics
1.1 How CCDSL differs from JavaScript
- Semicolons are mandatory, as in C/C++.
- Numbers are not all double-precision floats:
Int,Ulonglong, andDoubleare distinct types, needed to represent disk sizes and network traffic measured in bytes without losing precision. - Associative arrays are a dedicated
Maptype. - Arrays are two-dimensional but can be used as one-dimensional (
a[10, 11]anda[10]are both valid). - There is a distinct
Listtype. - Namespaced/static-style functions use C++ notation (
JSON::parse(text),cluster::hosts()) rather than JavaScript's dot notation. - Variables created on the fly inside a function are local to that function, not global.
- The language implements a C-like
#includepreprocessor directive.
1.2 Types
Built-in types: Int, Bool, Double, Ulonglong, String, Error, Map, List, Array, CmonHost, CmonMySqlHost, CmonGaleraHost, CmonGroupReplHost, CmonPostgreSqlHost, CmonNdbHost, CmonMongoHost, CmonMaxScaleHost, CmonProxySqlHost, CmonAdvice, CmonClusterConfig, CmonFile, CmonGraph, CmonJob, CmonRegExp, CmonDateTime.
1.3 Literals
Boolean literals: true and false.
String literals — single or double quoted; single-quoted strings may contain double quotes and vice versa. Adjacent string literals concatenate at parse time, C-style:
var carName1 = "Volvo XC60";
var carName2 = 'Volvo XC60';
var answer1 = "It's alright";
var answer2 = "He is called 'Johnny'";
var a = "one "
"two "
"three";
passed = a == "one two three";
Integer literals, including hex:
Unsigned long long literals — an integer literal too large for Int is automatically stored as Ulonglong. A literal can also be forced to this type with an ull suffix:
var a = 91872698761001;
var b = 10ull;
passed = a.typeName() == "Ulonglong" && b.typeName() == "Ulonglong";
Double literals — any number not fitting an integer type:
Error literals — sentinel values representing "not a real value" conditions:
| Literal | Meaning |
|---|---|
#NULL |
Null value |
#DIV/0! |
Division by zero |
#VALUE! |
Type mismatch, e.g. the log of a string |
#REF! |
Invalid variable reference, missing variable |
#NAME? |
Function name not found |
#NUM! |
Invalid numerical value, e.g. sqrt(-1) |
#N/A |
Value is not available — this is what an unpassed argument to main() evaluates to |
#SYNTAX! |
Syntax error |
#ARGS! |
Wrong argument count for a function call |
Map literals — associative arrays that can hold any value type, including nested maps, indexed by string keys:
var a = {};
a["one"] = {};
a["one"]["two"] = "value";
passed = a.typeName() == "Map" && a["one"]["two"] == "value";
var testmap = {};
testmap["key1"] = "test";
testmap["key2"] = "test";
keys = testmap.keys();
for (i = 0; i < keys.size(); ++i) {
print(keys[i] + ": " + testmap[keys[i]]);
}
Regular expression literals — same syntax as JavaScript, typed as CmonRegExp:
var regexp = /([0-9]+)x([0-9]+)/i;
var string = "s: 640x480";
string.replace(regexp, "$2x$1");
// string === "s: 480x640"
1.4 Preprocessor directives
#include works as in C/C++. Two kinds of path are used:
- Built-in headers —
cmon/alarms.h,cmon/io.h,cmon/graph.h— bring named constants into scope (severities and alarm types,toString()format specifiers, graph plot styles). - Script includes, such as
common/mysql_helper.jsorcommon/helpers.js— shared library files, included by their path relative to the project root, that provide reusable helper functions.
#pragma once protects an include file from being included multiple times.
2. Program structure
2.1 Entry points
main(...)— if a script definesmain(), it runs after any top-level statements. The arguments passed tomain()come from the calling environment; the return value ofmain()becomes the script's exit status, unlessexit()was called first.
var global1 = 10;
function main(arg1) {
return arg1 == "UtCmonImperative" && global1 === 10 && global2 === 11;
}
var global2 = 11;
exit(exitstatus)— ends script execution and returns the given exit status to the Cmon environment; a normal program termination.abort()— aborts execution and presents a backtrace showing exactly whereabort()was called.
If a function is called with the wrong number of arguments, the call evaluates to an #ARGS! error rather than raising an exception.
2.2 Input/output functions
print([value]...)— prints all arguments as one message atinfoseverity, and returns the printed string.warning([value]...)— same, atwarningseverity.error([value]...)— same, atcriticalseverity.
3. Advisors, programs, and widgets
A CCDSL script is one .js file. Its location within the bundle and its calling convention determine what kind of script it is.
3.1 Advisors
An advisor lives under a category directory (for example mysql/innodb/, host/, ndbcluster/), runs on a schedule, and produces CmonAdvice for the cluster it examines. An advisor takes no arguments (or, occasionally, one optional flag) and follows this shape:
#include "common/mysql_helper.js"
#include "cmon/alarms.h"
var DESCRIPTION = "One or two sentences describing what this checks and why it matters.";
var TITLE = "Short human-readable title";
var OK_THRESHOLD = 999;
var WARNING_THRESHOLD = 990;
function main()
{
var hosts = cluster::mySqlNodes();
var advisorMap = {};
for (idx = 0; idx < hosts.size(); ++idx)
{
host = hosts[idx];
if (!host.connected())
continue;
var advice = new CmonAdvice();
advice.setHost(host);
advice.setTitle(TITLE);
// ... compute a value, decide a severity ...
advice.setSeverity(Ok); // Ok, Warning, or Critical
advice.setJustification("measured value and how it was computed");
advice.setAdvice("what the administrator should do, if anything");
advisorMap[idx] = advice;
}
return advisorMap;
}
DESCRIPTION, TITLE, and any threshold or message constants are declared as top-level vars before main(). main() returns a Map keyed by loop index, with one CmonAdvice per examined host — advisorMap[idx] = advice for each host, return advisorMap; at the end. This is the return contract every advisor implements, including ones that only ever produce a single entry.
cluster::hosts() can return the same physical host more than once — for example once by its controller role and once by its database role. When an advisor must examine each physical host only once, it deduplicates by host name:
var examinedHostnames = "";
for (idx = 0; idx < hosts.size(); ++idx) {
host = hosts[idx];
if (!host.connected()) continue;
if (examinedHostnames.contains(host.hostName())) continue;
examinedHostnames += host.hostName();
...
}
Rate- or ratio-based checks should confirm the server has been running long enough, and handling enough traffic, for the computed value to be meaningful, before comparing it against a threshold — see checkPrecond() in section 21.1.
Some advisors also remediate: they read a configuration value, compute a corrected one, apply it immediately with setGlobalVariable() (a runtime SET GLOBAL), and persist it with host.config() → config.setVariable() → config.save() (a config-file change, which only takes effect after a restart). Doing both keeps the running value and the on-disk value consistent.
An advisor that only applies to certain cluster types can check cluster::type() and return a single Ok advice with an explanatory justification for cluster types it does not support:
var clusterType = cluster::type().toString();
if (clusterType != "REPLICATION") {
var advice = new CmonAdvice();
advice.setSeverity(Ok);
advice.setJustification("Advisor supports only REPLICATION clusters.");
advice.setAdvice("Nothing to do.");
advisorMap[0] = advice;
return advisorMap;
}
An advisor is not required to produce CmonAdvice at all — a script whose purpose is a human-readable report can simply print() its output and return true;.
3.2 Programs
A program lives under a programs/ directory, is invoked on demand with explicit arguments (rather than on a schedule), and validates every argument before doing any work:
/**
* explain query
* Format: db query
* db : "mydb"
* query : "select 1"
*/
function main(db, q)
{
var result = [];
if (db.toString() == "" || db == #N/A || db.empty()) {
result["error_msg"] = "Argument 'db' not specified";
print(result["error_msg"]);
exit(result);
}
if (q.toString() == "" || q == #N/A || q.empty()) {
result["error_msg"] = "Argument 'query' not specified";
print(result["error_msg"]);
exit(result);
}
// ... perform the work, then exit(result) ...
}
An argument is checked against all three conditions — x.toString() == "", x == #N/A, and x.empty() — since any one check alone can miss a case depending on how the caller passed or omitted the argument. A doc comment above main() documenting the expected argument order and format is standard practice.
3.3 Widgets
A widget lives under a widgets/ directory and is invoked by a user-interface element rather than a schedule. Widgets follow the same argument-validation convention as programs and are typically grouped into subdirectories by the UI surface that calls them (for example widgets/config/, widgets/qm/ for query-monitor actions such as explain, kill, and processlist, widgets/schema/, widgets/usermgmt/). Because a widget backs a specific UI element, its header comment should note that modifying it can affect that UI feature.
4. Cluster and host objects
4.1 Cluster functions
| Function | Returns |
|---|---|
cluster::hosts() |
every host considered part of the cluster, including the controller's own host, as plain CmonHost objects (see below) |
cluster::mySqlNodes() |
the CmonMySqlHosts of the cluster |
cluster::galeraNodes() |
the CmonGaleraHosts of the cluster |
cluster::groupReplNodes() |
the CmonGroupReplHosts of the cluster (MySQL Group Replication) |
cluster::postgreSqlNodes() |
the CmonPostgreSqlHosts of the cluster |
cluster::mongoNodes() |
the CmonMongoHosts of the cluster |
cluster::ndbdNodes() |
the CmonNdbHosts of the cluster (NDB Cluster data nodes) |
cluster::maxscaleNodes() |
the CmonMaxScaleHosts of the cluster |
cluster::proxysqlNodes() |
the CmonProxySqlHosts of the cluster |
cluster::id() |
the cluster's numeric ID |
cluster::name() |
the cluster's display name |
cluster::type() |
the cluster type, as a string (for example "REPLICATION", "GALERA", "MONGODB") |
cluster::state() |
the numeric cluster state |
cluster::vendor() |
the database vendor of the cluster, as a string (for example "percona", "mariadb") |
cluster::rollingRestart() |
restarts the nodes of the cluster one at a time, without stopping it |
cluster::hosts() always builds its result as literal CmonHost objects, regardless of the underlying node's real type — including for node types that have no dedicated cluster::*Nodes() accessor of their own (Redis/Valkey, MSSQL, Elasticsearch, and ClickHouse nodes; see section 4.4). Every CmonHost function documented in section 4.2 works on the result, but subtype-specific functions (section 4.3) require going through the matching cluster::*Nodes() call instead.
4.2 CmonHost tag functions
Identity and status:
host.hostName() // string
host.port() // int, usually the SQL server port
host.ipAddress() // string, IPv4 address
host.dataHostName() // string, the hostname used for data-layer connections
host.internalHostName() // string, the hostname used for internal cluster communication
host.clusterId() // int
host.nodeType() // "controller" | "mysql" | "galera" | "mongo" | "maxscale" | "proxysql" |
// "redis" | "valkey" | "mssql" | "elastic" | "clickhouse" | ...
host.hostStatus() // string, human-readable node status
host.role() // "controller" | "master" | "slave" | ...
host.connected() // boolean
host.message() // human-readable status description
host.serverVersion() // SQL server version string, or the Cmon version for the controller host
host.isAtLeastVersion(versionString) // boolean, minimum-version check
host.dataDir() // string, database data directory
host.distributionName() / .distributionCodeName() / .distributionRelease()
host.toMap() // the host's properties as a Map
host.toJSonString() // the host as a JSON string
host.toMap() returns a map of the host's properties; keys commonly used include connected, nodetype, isgalera, readonly, and, for Galera hosts, a nested galera map with galerastatus and localstatusstr (for example "Synced").
Statistics:
host.memoryInfo() // latest memory statistics, as a Map
host.memoryStats(startTime, endTime) // memory statistics over a period, as a List
host.sqlInfo() // latest SQL server statistics, as a Map
host.sqlStats(startTime, endTime) // SQL server statistics over a period, as a List
host.mongoStats(startTime, endTime) // MongoDB statistics over a period, as a List
host.networkInfo() // one Map per monitored network interface
host.diskInfo() // one Map per monitored disk partition
host.diskStats(startTime, endTime, [deviceName]) // disk statistics over a period, as a List
host.cpuInfo() // one Map per CPU core
host.cpuStats(startTime, endTime, [coreId]) // CPU statistics over a period, as a List
Each *Stats(startTime, endTime) call returns a List of per-sample Maps (each including a created timestamp field). Convert a List to an Array for statistical processing with .toArray(fieldNames) — see section 6.
Execution and configuration:
host.system(command) // shell command; {success, errorMessage, result}
host.executeSqlQuery(query) // SELECT-style query; {success, errorMessage, result}
host.executeSqlCommand(sqlCommand) // INSERT/UPDATE/DDL-style command; {success, errorMessage}
host.executeMongoQuery(dbname, query) // Mongo query; {success, errorMessage, result}
host.sqlPing([timeoutSeconds]) // boolean
host.sqlSystemVariable(name) / host.sqlSystemVariables() // cached SHOW GLOBAL VARIABLES (SHOW ALL on PostgreSQL)
host.sqlStatusVariable(name) / host.sqlStatusVariables() // cached SHOW GLOBAL STATUS
host.config([fileName]) // configuration as a CmonClusterConfig, see [section 9](#9-configuration-files-cmonclusterconfig)
Alarms:
host.checkValue(type, value) // checks a value against a registered check type, raising/clearing as needed
host.raiseAlarm(type, severity, [message])
host.clearAlarm(type)
host.alarms() // active alarms for this host
4.3 Subtype tag functions
CmonMySqlHost inherits all CmonHost properties and functions, and adds:
host.isGalera() // boolean
host.uptime() // ulonglong, SQL server uptime in seconds
host.readOnly() // boolean, the 'read_only' SQL variable
Everywhere a CmonHost function is documented, it is also usable on CmonMySqlHost, CmonGaleraHost, CmonGroupReplHost, CmonPostgreSqlHost, CmonNdbHost, CmonMongoHost, CmonMaxScaleHost, and CmonProxySqlHost — this is the complete set of CmonHost subclasses a script can obtain a typed handle for, via the matching cluster::*Nodes() function in section 4.1 or via getSelf() inside a subtype-specific tag function.
4.4 Node types without a typed host class
Redis/Valkey, MSSQL (Availability Group or standalone), Elasticsearch, and ClickHouse nodes are fully managed cluster types, but none of them has a cluster::*Nodes() accessor or a corresponding entry in section 1.2's built-in type list — there is no cluster::redisNodes(), cluster::mssqlNodes(), cluster::elasticNodes(), or cluster::clickhouseNodes(), and no way to construct a typed handle for one of these hosts from a script.
These nodes are still reachable through cluster::hosts(), which returns them as plain CmonHost objects, distinguishable by host.nodeType() ("redis", "valkey", "mssql", "elastic", or "clickhouse"). Every generic CmonHost function from section 4.2 works on them normally — hostName(), port(), connected(), system(), raiseAlarm()/clearAlarm(), diskInfo()/diskStats(), cpuInfo()/cpuStats(), memoryInfo()/memoryStats(), networkInfo(), config(). What isn't available is a type-specific query method equivalent to executeSqlQuery() or executeMongoQuery(), so engine-specific data has to come from shelling out with host.system() to that engine's own CLI or REST API:
var hosts = cluster::hosts();
for (idx = 0; idx < hosts.size(); ++idx) {
host = hosts[idx];
if (host.nodeType() != "redis" && host.nodeType() != "valkey")
continue;
var retval = host.system("redis-cli -p " + host.port() + " INFO replication");
if (retval["success"])
print(retval["result"]);
}
The same pattern (a real, managed node type reached only through cluster::hosts() plus host.system(), with no dedicated query method) already applies to MaxScale-adjacent tooling such as maxscale/ms_admin.js, which shells out to maxctrl rather than calling a typed MaxScale query function.
5. Executing SQL commands and queries
host.executeSqlQuery(), host.executeSqlCommand(), and host.system() all return a Map with a success boolean, an errorMessage string, and, for queries and shell commands, a result value.
function getSqlVariable(host, variableName)
{
var query = "SHOW GLOBAL STATUS LIKE '" + variableName + "'";
var retval = host.executeSqlQuery(query);
if (!retval["success"]) {
print("ERROR:", retval["errorMessage"]);
return #N/A;
}
var value = retval["result"][0, 1]; // two-dimensional indexing: row 0, column 1
if (value.looksInteger()) return value.toInt();
if (value.looksULongLong()) return value.toULongLong();
if (value.looksDouble()) return value.toDouble();
return value;
}
host.executeSqlQuery() and host.executeSqlCommand() work identically on CmonMySqlHost, CmonGaleraHost, CmonGroupReplHost, and CmonPostgreSqlHost — a PostgreSQL advisor can run the same query/command pattern shown here against a CmonPostgreSqlHost, substituting PostgreSQL SQL for MySQL SQL (for example SELECT * FROM pg_stat_activity in place of SHOW GLOBAL STATUS).
Executing a shell command:
function listFiles(host)
{
var retval = host.system("ls -lha /home");
if (!retval["success"])
error("ERROR: ", retval["errorMessage"]);
print("Host : ", host.hostName());
print("Result : ", retval["result"]);
print("Success : ", retval["success"]);
return retval["success"];
}
Executing a query against the Cmon database itself (rather than a managed host):
var retval = CmonDb::executeSqlQuery("select * from mysql_states;");
if (!retval["success"])
error("Executing SQL query failed: ", retval["errorMessage"]);
for (idx = 0; idx < retval["result"].rows(); ++idx) {
print(retval["result"][idx, 2]);
}
6. Obtaining and processing statistical information
#include "cmon/io.h"
function toGigaBytes(value) { return value / (1024 * 1024 * 1024); }
function main()
{
var host = cluster::hosts()[0];
var endTime = CmonDateTime::currentDateTime();
var startTime = endTime - 10 * 60;
var stats = host.memoryStats(startTime, endTime);
for (idx = 0; idx < stats.size(); ++idx) {
map = stats[idx];
created = CmonDateTime::fromUnixTime(map["created"]);
ramfree = toGigaBytes(map["ramfree"]);
print(created.toString(LongTimeFormat), " ", ramfree.toString(TwoDecimalNumber), "GBytes");
}
return true;
}
For most tasks, converting a List to an Array with .toArray(fieldName) and using the high-level statistical functions is more efficient than looping over individual samples:
function printUtil(host, startTime, endTime)
{
var list = host.memoryStats(startTime, endTime);
var array = list.toArray("memoryutilization");
var min = min(array);
var max = max(array);
var ninth = percentile(array, 0.9);
print(host.hostName(), " ",
min.toString(TwoDecimalPercent), " - ",
ninth.toString(TwoDecimalPercent), " - ",
max.toString(TwoDecimalPercent));
return true;
}
.toArray() also accepts a comma-separated list of fields to produce a multi-column array, useful for graphing or forecasting aligned x/y series:
var stats = host.sqlStats(startTime, endTime);
var array = stats.toArray("created,interval,COM_SELECT,COM_INSERT");
Linear forecasting from historical data:
var list = host.diskStats(startTime, endTime, device);
var freeOverTime = list.toArray("free");
var timeOfEachSample = list.toArray("created");
var expectedFree = forecast(futureTime.toInt(), freeOverTime, timeOfEachSample);
expectedFree = expectedFree < 0 ? 0 : expectedFree; // linear regression can predict below zero
7. Advice: CmonAdvice
CmonAdvice represents an action to be taken by the administrator, together with the information needed to understand why it was raised.
var advice = new CmonAdvice();
advice.setHost(host);
advice.setTitle("Short title");
advice.setSeverity(Warning); // Ok, Warning, or Critical — from #include "cmon/alarms.h"
advice.setJustification("Measured 96% CPU over the last 60 minutes.");
advice.setAdvice("Investigate top queries or scale up the instance.");
| Function | Description |
|---|---|
advice.setTitle(title) / advice.title() |
a short description of the advice |
advice.setCreator(creator) / advice.creator() |
the owner that created the advice; defaults to the source file |
advice.setJustification(text) / advice.justification() |
a detailed, human-readable description of the measured value and why an action is suggested |
advice.setAdvice(text) / advice.advice() |
the recommended action |
advice.setSeverity(severity) / advice.severity() |
the severity level: Ok (0), Warning (1), or Critical (2) |
advice.setHost(host) |
the host the advice was generated for |
advice.toString(format) supports these format specifiers:
| Specifier | Meaning |
|---|---|
%t |
the title |
%j |
the justification |
%a |
the advice text |
%c |
the creator |
%h |
the host name, if set |
%E |
a multi-line summary of all of the above |
An advisor's main() returns a Map keyed by loop index, holding one CmonAdvice per examined host: advisorMap[idx] = advice;, then return advisorMap;.
8. Alarms
#include "cmon/alarms.h" provides the severity constants (Ok, Warning, Critical) and a set of predefined alarm-type constants that can be raised directly, without registration:
#include "cmon/alarms.h"
host.raiseAlarm(HostDiskUsage, Warning, "message");
host.clearAlarm(HostDiskUsage);
Predefined alarm types include HostDiskUsage, StorageUtilizationPrediction, MySqlReplicationConfigProblem, ClusterSystemCheck, MySqlAdvisor, MySqlTimeZoneCheck, StorageMyIsam, MySqlTableAnalyzer, MySqlIndexAnalyzer, and MySqlAutoIncrementAnalyzer, among others. A condition specific to an engine that has no matching predefined type (for example PostgreSQL table bloat) needs a custom alarm type registered as shown below before it can be raised with host.raiseAlarm(); until such a type exists, an advisor can only report the condition through CmonAdvice severity.
For conditions that don't match a predefined type, register a custom one:
#include "cmon/alarms.h"
function myAlarm() {
return Alarm::alarmId(
Node, true,
"Computer is on fire",
"The computer is on fire, it is on flames.",
"Pour some water on it.");
}
var myAlarmId = myAlarm();
var host = cluster::hosts()[0];
host.raiseAlarm(myAlarmId, Critical);
Alarm::alarmId(category, isHost, title, message, recommendation)registers a new alarm type (or returns the existing one if an identical type is already registered) and returns its ID.Alarm::checkId(category, isHost, warningLevel, criticalLevel, title, message, recommendation)registers a check type — an alarm with warning and critical numeric thresholds — for use withhost.checkValue(type, value).
Reading active alarms:
#include "cmon/alarms.h"
var alarms = host.alarms();
var found = false;
for (idx = 0; idx < alarms.size(); ++idx) {
if (alarms[idx]["title"] == "MySQL advisor alarm") {
found = true;
break;
}
}
An alarm that is raised in a failing branch should be cleared with a matching clearAlarm() call in the corresponding healthy branch, so that it does not remain active once the underlying condition is resolved.
9. Configuration files: CmonClusterConfig
function getConfiguredClientPort(host)
{
var config = host.config();
var variable = config.variable("port");
for (idx = 0; idx < variable.size(); ++idx) {
print("*** section : ", variable[idx]["section"]);
print("*** value : ", variable[idx]["value"]);
print("*** location : ", variable[idx]["filepath"], ":", variable[idx]["linenumber"]);
if (variable[idx]["section"] == "client")
return variable[idx]["value"].toInt();
}
return #N/A;
}
| Function | Description |
|---|---|
host.config([fileName]) |
loads the configuration from the host, together with its include files and directories, as a CmonClusterConfig |
config.errorMessage() |
a human-readable message describing the state of the last operation |
config.variable([variableName]) |
a List of Maps (variablename, linenumber, value, filepath, section); returns every variable in the file if no name is given |
config.setVariable(section, variableName, value) |
sets (or adds) a variable in the given section; changes the in-memory configuration object only |
config.save() |
saves the configuration back to the original host(s) and file name(s); returns {success, errorMessage} |
Changing a variable with config.setVariable() and config.save() updates the configuration file; it does not change the running value. To apply a dynamic variable immediately as well, issue a SET GLOBAL through host.executeSqlCommand() in addition to saving the configuration file.
10. Files: CmonFile
CmonFile represents a single file — either local to the controller, or on a managed host — and provides read-only inspection and reading functions. It is a different tool from CmonClusterConfig (section 9): CmonClusterConfig understands a config file's section/variable/value structure, while CmonFile reads any file as raw text or lines, or just inspects its metadata.
var file = new CmonFile("/etc/mysql/my.cnf", host); // a file on a managed host
// var file = new CmonFile("/etc/cmon.cnf"); // a file on the controller itself
if (!file.exists()) {
print("File not found: ", file.fullPath());
} else if (file.isReadable()) {
var content = file.readTxtFile();
var lines = file.readLines();
print(file.fileName(), " is ", content.length(), " bytes, ", lines.size(), " lines");
}
| Function | Description |
|---|---|
new CmonFile(path, [host]) |
opens path; with no host argument the file is on the controller, otherwise on the given CmonHost |
file.fileName() |
the file name, without its directory |
file.baseName() |
the file name without its extension |
file.fullPath() |
the full, absolute path |
file.isRemote() |
true if the file is on a managed host rather than the controller |
file.exists() |
true if the file exists |
file.isDirectory() |
true if the path is a directory |
file.isExecutable() |
true if the file has execute permission |
file.isReadable() |
true if the file has read permission |
file.readTxtFile() |
the file's contents as a String |
file.readLines() |
the file's contents as a List of lines |
file.errorString() |
a human-readable message describing the last failed operation |
11. Jobs: CmonJob
A CmonJob represents a task the Cmon controller executes, usually asynchronously: the script builds a CmonJob, sets its properties, and enqueues it; the controller then executes it and reports the results to the UI.
var job = CmonJob::createBackupJob("127.0.0.1", "/var/tmp");
job.setBackupMethod("mysqldump");
var passed = job.enqueue();
if (passed !== true) {
error("ERROR: ", job.errorString());
exit(false);
}
| Function | Description |
|---|---|
job.enqueue() |
checks the job for consistency and sends it to the execution queue; returns true on success |
job.errorString() |
a human-readable error, or an empty string if there was none |
job.jobId() |
the job's numeric ID; valid (greater than 0) only after a successful enqueue() |
CmonJob::getJob(jobId) |
reads a previously enqueued job from the Cmon database; #N/A if not found |
CmonJob::createBackupJob(host, dir) |
creates a job that backs up a host's data |
CmonJob::createDoCheckJob() |
creates a job that runs schema/index checks on the controller |
CmonJob::createAddNodeJob(hostName, [install], [configFile]) |
creates a job that adds a new node to the cluster |
job.setBackupMethod(method) |
null/"auto"/"none" (default method), "mysqldump", "xtrabackupfull", "xtrabackupincr", or "pgdump" |
job.setCompression(compression) |
whether the backup file should be compressed |
job.setIncludeDatabases(value) |
which databases to include |
job.setIsCcStorage(value) |
whether the controller should store the backup file |
job.setNetcatPort(value) |
the netcat port used when copying the backup file over the network |
12. Graphs: CmonGraph
#include "cmon/graph.h"
var host = cluster::hosts()[0];
var endTime = CmonDateTime::currentDateTime();
var startTime = endTime - 10 * 60;
var stats = host.sqlStats(startTime, endTime);
var array = stats.toArray("created,interval,COM_SELECT,COM_INSERT");
for (idx = 0; idx < array.columns(); idx++) {
array[5, idx] = 1000 * array[2, idx] / array[1, idx];
array[6, idx] = 1000 * array[3, idx] / array[1, idx];
}
var graph = new CmonGraph;
graph.setXDataIsTime();
graph.setTitle("SQL Statistics " + host.toString());
graph.setSize(800, 600);
graph.setPlotLegend(1, "Select (1/s)");
graph.setPlotColumn(1, 0, 5);
graph.setPlotStyle(1, Impulses);
graph.setPlotLegend(2, "Insert (1/s)");
graph.setPlotColumn(2, 0, 6);
graph.setPlotStyle(2, Impulses);
graph.setData(array);
exit(graph);
| Function | Description |
|---|---|
graph.setTitle(title) |
text shown at the top of the graph image |
graph.setSize(width, height) |
size of the generated graph, in pixels |
graph.setXDataIsTime([boolean]) |
whether X-axis values should be printed as date/time |
graph.setPlotLegend(plotIdx, legend) |
legend text for the given plot |
graph.setPlotColumn(plotIdx, xColumn, yColumn) |
which array columns feed the given plot |
graph.setPlotStyle(plotIdx, style) |
plot rendering style; available styles are defined in cmon/graph.h |
Plot indices are 1-based.
13. JSON functions and the Cmon database
var req = new Map;
req["operation"] = "clusters";
var retval = JSON::postRequest("http://localhost:9500/0/clusters", req);
print("retval of clusters:\n" + retval);
| Function | Description |
|---|---|
JSON::parse(text) |
parses a JSON string, returns it as a Map |
JSON::toString(map) |
converts a Map to a well-formatted JSON string |
JSON::postRequest(url, map) |
sends a POST request to the given URL with the JSON-encoded map as its body, returning the reply |
CmonDb::executeSqlQuery(query) |
executes a query against the Cmon database (the controller's own metadata store); returns {success, errorMessage, result} |
14. Date and time: CmonDateTime
CmonDateTime is distinct from JavaScript's Date type. It supports direct arithmetic in seconds:
var endTime = CmonDateTime::currentDateTime();
var startTime = endTime - 60 * 60; // one hour ago
var futureTime = endTime + 2 * 24 * 3600; // two days from now
| Function | Description |
|---|---|
CmonDateTime::currentDateTime() |
the current real-time clock time from the controller host |
CmonDateTime::fromUnixTime(time) |
converts a Unix timestamp to a CmonDateTime |
CmonDateTime::fromString(string) |
parses a date/time string |
dateTime.second() / .minute() / .hour() |
time-of-day components |
dateTime.month() |
the month, 1–12 |
dateTime.year() |
the year, e.g. 2024 |
dateTime.weekday() |
day of the week, Sunday = 1 … Saturday = 7 |
dateTime.timeZone() |
seconds to add to local time to get UTC |
dateTime.dayLight() |
seconds to add for daylight saving time |
toString([format]) accepts either a named format constant from cmon/io.h, or a strftime()-style format string:
#include "cmon/io.h"
var dateTime = CmonDateTime::fromUnixTime(1424686003);
str01 = dateTime.toString(FileNameFormat); // "2015-02-23_110643"
str03 = dateTime.toString(LogFileFormat); // "Feb 23 11:06:43"
str04 = dateTime.toString(MySqlLogFileFormat); // "2015-02-23 11:06:43"
str09 = dateTime.toString(ShortDateFormat); // "02/23/15"
str10 = dateTime.toString(LocalDateTimeFormat); // "Mon Feb 23 11:06:43 2015"
str11 = dateTime.toString(EmailDateTimeFormat); // "Mon, 23 Feb 2015 11:06:43 +0100"
15. General value functions
Available on any value:
| Function | Description |
|---|---|
value.typeName() |
the type name of the value |
value.toString([formatId]) |
converts to string, optionally using a format specifier from cmon/io.h |
value.empty() |
true if a string has no characters, or a container has no items |
value.size() |
character count for strings, item count for containers; for an Array, the number of columns |
value.isNull() |
true for a null string, e.g. an unset SQL value |
value.isInvalid() |
true if the value was never set |
value.isString() / .isInt() / .isULongLong() / .isDouble() / .isBoolean() / .isNumber() / .isError() / .isMap() / .isList() / .isArray() |
type checks |
value.toInt() / .toULongLong() / .toDouble() / .toBoolean() |
conversions |
#include "cmon/io.h"
var theDouble = 42.0;
var str1 = theDouble.toString(TwoDecimalNumber); // "42.00"
var str2 = theDouble.toString(FourDecimalNumber); // "42.0000"
16. String functions
Tag functions on a string value:
| Function | Description |
|---|---|
string.length() |
number of characters |
string.indexOf(substring, [start]) |
position of the first match, or -1 |
string.split(separator) |
array of substrings |
string.substr(begin, length) |
substring extraction |
string.trim() |
removes leading/trailing whitespace |
string.toLowerCase() / .toUpperCase() |
case conversion |
string.contains(substring) |
boolean |
string.replace(from, to) |
substring replacement |
string.startsWith(other) / .endsWith(other) |
boolean, prefix/suffix check |
string.leftAlign(length) / .rightAlign(length) |
pads with trailing/leading spaces up to length (capped at 1000) — left- or right-aligns the text within a fixed-width field |
string.search(pattern) |
position of the first match of pattern (a string or a CmonRegExp), or -1 |
string.looksInteger() / .looksULongLong() / .looksDouble() / .looksBoolean() / .looksEmail() / .looksIpAddress() |
format checks |
string.toInteger() / .toULongLong() |
conversions |
Free functions: asc(text), char(number) / chr(number), left(text, n), right(text, n), startswith(text1, text2), endwith(text1, text2), mid(text, start, length), escape(text) / unescape(text), upper(text) / lower(text), trim(text), len(text), concatenate(text, [text]...).
17. Mathematical, array, and statistical functions
Mathematical: rand(), pi(), degrees(n), radians(n), sign(n), sin/asin/sinh, cos/acos/cosh/acosh, tan/atan/tanh/atanh, fisher(n)/fisherinv(n), log(n, [base]), sqrt(n), abs(n), exp(n), floor(n, [significance]), ceiling(n, [significance]), round(n, digits), roundup(n, digits), rounddown(n, digits), mround(n, multiple), even(n), iseven(n), odd(n), isodd(n), convert(n, fromUnit, toUnit) (byte, kbyte, mbyte, gbyte, tbyte, celsius, kelvin, fahrenheit, hz, mhz, ghz).
Array: choose(position, value, [value]...), transpose(array), filter rows(array, column, value), columns(array), rows(array), vlookup(value, array, column, [notExact]), hlookup(value, array, column, [notExact]), match(value, array, [matchType]).
Statistical — accept either individual arguments or arrays:
count, countblank, min, max, sum, sumsq, product, average, geomean, mode, emaverage(alpha, ...), median, percentile(array, [n]), small(array, n), large(array, n), stdev, avedev, pearson(array1, array2), correl(array1, array2), covar(array1, array2), devsq, var, forecast(x, knownYValues, knownXValues), linest(knownYValues, knownXValues), slope(knownYValues, knownXValues), intercept(knownYValues, knownXValues).
Value classification: iserr(value), isnumeric(value), istext(value), isnumber(value), isarray(value).
18. Regular expression functions
CmonRegExp tag functions:
| Function | Description |
|---|---|
regexp.test(string) |
true if the pattern matches |
regexp.lastIndex() |
index where the next match check begins; 0 without the "global" modifier |
regexp.match(string) |
a List with the matched text at index 0, and sub-expression matches from index 1 |
regexp.exec(string) |
a Map with detailed match information |
toString() format specifiers for CmonRegExp: %r (the pattern itself), %j (pattern and modifiers in JavaScript notation, e.g. /[0-9]+/ig), %m (the matched string, if any), %nm (the nth matched sub-expression).
19. Cmon, license, and introspection functions
| Function | Description |
|---|---|
cmon::version() |
the Cmon version, as a string |
cmon::build() |
the Cmon build number |
cmon::uptime() |
seconds since Cmon started managing this cluster |
cmon::running() |
true if Cmon is managing this cluster |
cmon::hostname() |
hostname of the controller's computer |
cmon::domainname() |
domain name of the controller's computer |
license::statustext() |
a short description of the license status |
license::status() |
true if the cluster has a valid license |
license::expires() |
days remaining on the license; negative if expired or not found |
conf::values() |
the controller's own cmon.cnf-level configuration, as a Map |
CmonMetaType::getParamSpec(className, propertyName) |
the metadata (as a Map) describing one property of one internal class |
CmonClusterInfo::getClusterInfo([clusterId]) |
a CmonClusterInfo for the given cluster, or the current one if omitted |
CmonServer::containers() |
the containers known to this controller (LXC/cloud), as a CmonContainer |
CmonUser::currentUser() |
the CmonUser the script is running as |
conf::values() is how a script checks a controller-level toggle before doing expensive work — for example, several information_schema-based MySQL advisors check enable_is_queries before running:
var cmonConfig = conf::values();
if (cmonConfig.keys().contains("enable_is_queries") &&
!cmonConfig["enable_is_queries"].toBoolean())
{
print("Information_schema queries are not enabled.");
exit(result);
}
20. Advisor bundle structure
An advisor bundle is a tarball (for example s9s.tar.gz) containing a project.conf file at its root and a directory tree of .js scripts organized by category.
20.1 Directory layout
s9s.tar.gz
└── s9s/
├── project.conf
├── host/ # OS-level checks
│ └── widgets/
├── mysql/
│ ├── auto_tuners/ # advisors that also change configuration
│ ├── connections/
│ ├── galera/
│ ├── general/
│ ├── group_repl/
│ ├── health/
│ ├── i_s/ # information_schema-based checks
│ ├── innodb/
│ ├── p_s/ # performance_schema-based checks
│ ├── programs/ # on-demand scripts, not scheduled
│ ├── query_cache/
│ ├── replication/
│ ├── schema/
│ ├── security/
│ ├── table_cache/
│ └── widgets/
│ ├── config/
│ ├── qm/ # query monitor: explain, kill, processlist
│ ├── schema/
│ └── usermgmt/
├── mongodb/
│ ├── connections/, mmap/, replication/, sharding/
│ └── widgets/qm/
├── maxscale/
├── ndbcluster/
├── predictions/ # forward-looking checks
└── reports/ # print-only output, no CmonAdvice
common.tar.gz
└── common/
├── project.conf
├── helpers.js # generic, non-database-specific helpers
├── mysql_helper.js # SQL helpers shared by MySQL/Galera advisors
└── cmonrpc.js # helpers for calling the Cmon RPC API from a script
A shared library such as common/ contains helper .js files, included by the other bundles with #include "common/mysql_helper.js", and is not itself scheduled.
Neither tarball currently contains a category directory for PostgreSQL, Redis/Valkey, MSSQL, Elasticsearch, or ClickHouse; project.conf already reserves [schedules-postgresql_single] (see below) but it has no entries.
20.2 project.conf
[project]
# Increase this value to force cmon to reinstall the scripts (a backup
# of the previous version is made automatically) on the next start/upgrade.
version=20210922
# Increase this value when only the [schedules*] sections below changed.
schedule_version=1029
[schedules]
# Applies to every cluster type.
s9s/host/cpu_usage.js="*/30 * * * *"
s9s/host/disk_space_usage.js="*/30 * * * *"
s9s/host/swappiness.js="0 1 * * *"
[schedules-mysqlcluster]
s9s/mysql/connections/connections_used_pct.js="*/1 * * * *"
s9s/mysql/schema/schema_check_nopk.js="0 1 * * *"
[schedules-replication]
...
[schedules-group_replication]
...
[schedules-galera]
...
[schedules-mysql_single]
...
[schedules-postgresql_single]
...
[schedules-mongodb]
...
[project]holds aversionand aschedule_version. Increasingversioncausescmonto reinstall the bundle's scripts (backing up the previous versions first); increasingschedule_versionis sufficient when only the schedule sections changed.[schedules]lists scripts scheduled for every cluster type.[schedules-<clustertype>]lists scripts scheduled only for that cluster type;<clustertype>is the same cluster-type identifier used when the cluster was created (for examplemysqlcluster,replication,group_replication,galera,mysql_single,postgresql_single,mongodb).- Each entry's key is the script's path relative to the tarball root, including the top-level directory name, and its value is a quoted five-field cron expression.
- A script that is present in the bundle but commented out of its
[schedules*]entry is shipped but not scheduled by default. - Scripts under a
programs/orwidgets/directory are not listed in any[schedules*]section; they are invoked on demand instead. common/project.confcontains only a[project]section with aversion, since the common bundle is a shared library and has nothing to schedule.
21. Shared helper library (common/*.js)
21.1 common/mysql_helper.js
| Function | Description |
|---|---|
readVariable(host, name) |
runs SHOW GLOBAL VARIABLES LIKE '<name>'; returns the value, or false on error |
readStatusVariable(host, name) |
runs SHOW GLOBAL STATUS LIKE '<name>'; returns the value, or false on error |
getSingleValue(host, query) |
runs a query, returns the single scalar from its first row/column |
getValueMap(host, query) |
runs a query, returns the result set as a {row: {column: value}} map |
getResultSet(host, query) |
runs a query, returns the raw executeSqlQuery() reply |
executeSqlCommand(host, query) |
runs a command, prints any error, and returns true/false |
executeSqlCommand2(host, query) |
runs a command and returns the raw reply map |
setGlobalVariable(host, variable, value) |
builds and runs the appropriate SET GLOBAL variable=value statement, quoting the value correctly for numeric or string types |
mySleep(host, seconds) |
runs SELECT SLEEP(seconds) |
checkPrecond(host) |
returns true when the host has been up for more than an hour and is handling more than 10 queries per second — used to avoid evaluating rate- or ratio-based checks against a freshly started or idle server |
isSystemTable(name) |
true for performance_schema, information_schema, mysql, or sys |
checkHostVersion(host, versionPrefix) |
true if the host's server version starts with the given prefix |
isMySqlHost(host) |
true for MySQL or Galera node types |
isMySql80Host(host) / isMySql55Host(host) |
version-specific MySQL checks |
isMariaDb100Host(host) / isMariaDb101Host(host) / isMariaDb102Host(host) / isMariaDb103Host(host) / isMariaDb104Host(host) / isMariaDb10xHost(host) |
version-specific MariaDB checks |
Despite the filename, getSingleValue(), getValueMap(), and getResultSet() are just thin wrappers around host.executeSqlQuery() and carry no MySQL-specific logic, so they are safe to reuse from a PostgreSQL advisor. readVariable() and readStatusVariable() are MySQL-specific (SHOW GLOBAL VARIABLES/SHOW GLOBAL STATUS syntax) and have no PostgreSQL equivalent in this file.
21.2 common/helpers.js
| Function | Description |
|---|---|
hostMatchesFilter(host, hostAndPort) |
true if a host matches a "hostname:port" filter string, or if the filter is empty or "*" |
executeOnController(command) |
runs a shell command specifically on the host whose node type is controller |
isMongoDbPrimary(host) |
true if the given MongoDB host is currently the primary, using the version-appropriate admin command (hello on newer releases, isMaster on older ones) |
21.3 common/cmonrpc.js
Helpers for calling the Cmon RPC API from within a script, by running curl on the controller host:
| Function | Description |
|---|---|
getCmonController() |
returns the CmonHost whose role is controller, or false if none is found |
curl(controller, path, json) |
POSTs a JSON payload to the controller's RPC endpoint for the given cluster and path, returning the result |
mapToJSON(map) / JSONToMap(text) |
converts between Map and JSON text |
addNode(node) |
enqueues an addnode job for the given hostname via the RPC API |
setCmonrpcToken(json, clusterId) |
attaches the RPC authentication token for the given cluster ID to a request payload |
findUnusedHost() |
returns the first configured free host not already part of the cluster |
CMONRPC_HOST, CMONRPC_PORT, and CMONRPC_TOKEN are the controller's RPC host, port, and per-cluster authentication tokens, referenced by these helpers when building a request.