From 256d658a35c0c848c43a8ccdf7fb9dd391039c86 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 24 Aug 2026 11:23:20 +0100 Subject: [PATCH 1/7] Add PRTG Network Monitor plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Low-code plugin for the PRTG v1 HTTP API, indexing probes, groups, devices and sensors, with ten data streams and six out-of-the-box dashboards (estate overview, sites, and a perspective per object type). Notes on a few non-obvious choices, all confirmed against a live PRTG 26.3 instance: - Reads the `_raw` variant of every text column. PRTG's display columns carry markup — `message` is `
OK
`, `lastcheck` appends `[165 s ago]`. - `table.json` OLE datetimes are UTC, so they convert directly. `historicdata.json` reports only server-local wall-clock text with no UTC equivalent, which is why the PRTG time zone is configurable and why that stream needs a post-request script. - `content=channels` HTML-escapes `lastvalue` where `content=sensors` does not, so only Sensor Channels decodes entities. - Sensor History averages hourly beyond seven days. Measured at the 5-minute default, a 30-day range on a nine-channel sensor unpivots to roughly 7 MB and exceeds the response size limit. - Sensor counts come back as empty strings rather than zero, hence the `Number(x) || 0` guards in System Status. Co-Authored-By: Claude Opus 5 (1M context) --- .github/CODEOWNERS | 1 + cspell.json | 4 +- plugins/PRTG/v1/configValidation.json | 11 + plugins/PRTG/v1/custom_types.json | 30 ++ .../PRTG/v1/dataStreams/containerDevices.json | 183 +++++++++ .../PRTG/v1/dataStreams/containerSensors.json | 232 +++++++++++ plugins/PRTG/v1/dataStreams/devices.json | 171 ++++++++ plugins/PRTG/v1/dataStreams/groups.json | 159 ++++++++ plugins/PRTG/v1/dataStreams/logs.json | 91 +++++ plugins/PRTG/v1/dataStreams/probes.json | 122 ++++++ .../v1/dataStreams/scripts/sensorHistory.js | 81 ++++ .../PRTG/v1/dataStreams/sensorChannels.json | 66 +++ .../PRTG/v1/dataStreams/sensorHistory.json | 112 +++++ plugins/PRTG/v1/dataStreams/sensors.json | 219 ++++++++++ plugins/PRTG/v1/dataStreams/systemStatus.json | 124 ++++++ .../PRTG/v1/defaultContent/device.dash.json | 234 +++++++++++ .../PRTG/v1/defaultContent/group.dash.json | 270 +++++++++++++ plugins/PRTG/v1/defaultContent/manifest.json | 28 ++ .../PRTG/v1/defaultContent/overview.dash.json | 381 ++++++++++++++++++ .../PRTG/v1/defaultContent/probe.dash.json | 270 +++++++++++++ .../PRTG/v1/defaultContent/sensor.dash.json | 161 ++++++++ .../PRTG/v1/defaultContent/sites.dash.json | 374 +++++++++++++++++ plugins/PRTG/v1/docs/README.md | 133 ++++++ plugins/PRTG/v1/icon.svg | 32 ++ plugins/PRTG/v1/indexDefinitions/default.json | 131 ++++++ plugins/PRTG/v1/metadata.json | 50 +++ plugins/PRTG/v1/scopes.json | 50 +++ plugins/PRTG/v1/ui.json | 80 ++++ 28 files changed, 3799 insertions(+), 1 deletion(-) create mode 100644 plugins/PRTG/v1/configValidation.json create mode 100644 plugins/PRTG/v1/custom_types.json create mode 100644 plugins/PRTG/v1/dataStreams/containerDevices.json create mode 100644 plugins/PRTG/v1/dataStreams/containerSensors.json create mode 100644 plugins/PRTG/v1/dataStreams/devices.json create mode 100644 plugins/PRTG/v1/dataStreams/groups.json create mode 100644 plugins/PRTG/v1/dataStreams/logs.json create mode 100644 plugins/PRTG/v1/dataStreams/probes.json create mode 100644 plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js create mode 100644 plugins/PRTG/v1/dataStreams/sensorChannels.json create mode 100644 plugins/PRTG/v1/dataStreams/sensorHistory.json create mode 100644 plugins/PRTG/v1/dataStreams/sensors.json create mode 100644 plugins/PRTG/v1/dataStreams/systemStatus.json create mode 100644 plugins/PRTG/v1/defaultContent/device.dash.json create mode 100644 plugins/PRTG/v1/defaultContent/group.dash.json create mode 100644 plugins/PRTG/v1/defaultContent/manifest.json create mode 100644 plugins/PRTG/v1/defaultContent/overview.dash.json create mode 100644 plugins/PRTG/v1/defaultContent/probe.dash.json create mode 100644 plugins/PRTG/v1/defaultContent/sensor.dash.json create mode 100644 plugins/PRTG/v1/defaultContent/sites.dash.json create mode 100644 plugins/PRTG/v1/docs/README.md create mode 100644 plugins/PRTG/v1/icon.svg create mode 100644 plugins/PRTG/v1/indexDefinitions/default.json create mode 100644 plugins/PRTG/v1/metadata.json create mode 100644 plugins/PRTG/v1/scopes.json create mode 100644 plugins/PRTG/v1/ui.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c44719cf..886cf3ae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ plugins/Huntress/* @Deenk plugins/FantasyPremierLeague/* @TimWheeler-SQUP plugins/GoogleSheets/* @kieranlangton plugins/MetOffice/* @blackgrouse +plugins/PRTG/* @Deenk plugins/Phare/* @vinbab plugins/Postcoder/* @richbenwell plugins/RDAP/* @richbenwell diff --git a/cspell.json b/cspell.json index d563d67b..ab612a7f 100644 --- a/cspell.json +++ b/cspell.json @@ -5,6 +5,8 @@ "rdap", "whois", "rootly", - "UniFi" + "UniFi", + "PRTG", + "Paessler" ] } \ No newline at end of file diff --git a/plugins/PRTG/v1/configValidation.json b/plugins/PRTG/v1/configValidation.json new file mode 100644 index 00000000..c8f38187 --- /dev/null +++ b/plugins/PRTG/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "systemStatus" }, + "required": true, + "error": "Could not connect to PRTG. Check the PRTG URL is reachable from SquaredUp, and that the API key is correct and has not been deleted.", + "success": "Connected to PRTG successfully." + } + ] +} diff --git a/plugins/PRTG/v1/custom_types.json b/plugins/PRTG/v1/custom_types.json new file mode 100644 index 00000000..828e26ad --- /dev/null +++ b/plugins/PRTG/v1/custom_types.json @@ -0,0 +1,30 @@ +[ + { + "name": "PRTG Probe", + "sourceType": "PRTG Probe", + "icon": "tower-broadcast", + "singular": "Probe", + "plural": "Probes" + }, + { + "name": "PRTG Group", + "sourceType": "PRTG Group", + "icon": "sitemap", + "singular": "Group", + "plural": "Groups" + }, + { + "name": "PRTG Device", + "sourceType": "PRTG Device", + "icon": "server", + "singular": "Device", + "plural": "Devices" + }, + { + "name": "PRTG Sensor", + "sourceType": "PRTG Sensor", + "icon": "gauge", + "singular": "Sensor", + "plural": "Sensors" + } +] diff --git a/plugins/PRTG/v1/dataStreams/containerDevices.json b/plugins/PRTG/v1/dataStreams/containerDevices.json new file mode 100644 index 00000000..f2bf6b51 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/containerDevices.json @@ -0,0 +1,183 @@ +{ + "name": "containerDevices", + "displayName": "Devices in Probe or Group", + "description": "Devices beneath a probe or group, with status, host address and sensor counts by state", + "tags": [ + "Infrastructure", + "Status" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "devices" + }, + { + "key": "id", + "value": "{{object.rawId}}" + }, + { + "key": "columns", + "value": "objid,name,parentid,probe,group,host,status,active,tags,location,icon,totalsens,upsens,downsens,warnsens,pausedsens" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "devices", + "paging": { + "mode": "none" + } + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "PRTG Probe", + "PRTG Group" + ] + } + }, + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Device", + "shape": "string", + "role": "label" + }, + { + "name": "parentid", + "displayName": "Parent ID", + "shape": "string", + "visible": false + }, + { + "name": "probe", + "displayName": "Probe", + "shape": "string" + }, + { + "name": "group", + "displayName": "Group", + "shape": "string" + }, + { + "name": "host", + "displayName": "Host", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "totalsens_raw", + "displayName": "Total Sensors", + "shape": "number", + "role": "value" + }, + { + "name": "upsens_raw", + "displayName": "Up Sensors", + "shape": "number" + }, + { + "name": "downsens_raw", + "displayName": "Down Sensors", + "shape": "number" + }, + { + "name": "warnsens_raw", + "displayName": "Warning Sensors", + "shape": "number" + }, + { + "name": "pausedsens_raw", + "displayName": "Paused Sensors", + "shape": "number" + }, + { + "name": "location_raw", + "displayName": "Location", + "shape": "string" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": "string" + }, + { + "name": "icon", + "displayName": "Icon", + "shape": "string", + "visible": false + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/containerSensors.json b/plugins/PRTG/v1/dataStreams/containerSensors.json new file mode 100644 index 00000000..ebfd05cf --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/containerSensors.json @@ -0,0 +1,232 @@ +{ + "name": "containerSensors", + "displayName": "Sensors in Probe, Group or Device", + "description": "Sensors beneath a probe, group or device, with status, last reading and uptime", + "tags": [ + "Monitoring", + "Status" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "sensors" + }, + { + "key": "id", + "value": "{{object.rawId}}" + }, + { + "key": "columns", + "value": "objid,name,parentid,probe,group,device,type,status,message,lastvalue,lastcheck,interval,priority,active,tags,uptime,downtime" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "sensors", + "paging": { + "mode": "none" + } + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "PRTG Probe", + "PRTG Group", + "PRTG Device" + ] + } + }, + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Sensor", + "shape": "string", + "role": "label" + }, + { + "name": "parentid", + "displayName": "Device ID", + "shape": "string", + "visible": false + }, + { + "name": "device", + "displayName": "Device", + "shape": "string" + }, + { + "name": "group", + "displayName": "Group", + "shape": "string" + }, + { + "name": "probe", + "displayName": "Probe", + "shape": "string" + }, + { + "name": "type", + "displayName": "Sensor Type", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "message_raw", + "displayName": "Message", + "shape": "string" + }, + { + "name": "lastvalue", + "displayName": "Last Value", + "shape": "string" + }, + { + "name": "lastvalue_raw", + "displayName": "Last Value (Numeric)", + "shape": "number" + }, + { + "name": "lastcheck_raw", + "displayName": "Last Check (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "lastCheck", + "displayName": "Last Check", + "computed": true, + "shape": "date", + "valueExpression": "{{ Number($['lastcheck_raw']) > 0 ? new Date((Number($['lastcheck_raw']) - 25569) * 86400000).toISOString() : null }}" + }, + { + "name": "uptime_raw", + "displayName": "Uptime (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "uptimePercent", + "displayName": "Uptime", + "computed": true, + "role": "value", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ], + "valueExpression": "{{ $['uptime_raw'] === '' || $['uptime_raw'] === null || $['uptime_raw'] === undefined ? null : Number($['uptime_raw']) / 10000 }}" + }, + { + "name": "downtime_raw", + "displayName": "Downtime (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "downtimePercent", + "displayName": "Downtime", + "computed": true, + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ], + "valueExpression": "{{ $['downtime_raw'] === '' || $['downtime_raw'] === null || $['downtime_raw'] === undefined ? null : Number($['downtime_raw']) / 10000 }}" + }, + { + "name": "interval_raw", + "displayName": "Scanning Interval", + "shape": "seconds" + }, + { + "name": "priority_raw", + "displayName": "Priority", + "shape": "number" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": "string" + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + }, + { + "sourceId": "parentid", + "sourceType": "PRTG Device", + "name": "device" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/devices.json b/plugins/PRTG/v1/dataStreams/devices.json new file mode 100644 index 00000000..67f61485 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/devices.json @@ -0,0 +1,171 @@ +{ + "name": "devices", + "displayName": "Devices", + "description": "All PRTG devices with their status, host address and sensor counts by state", + "tags": [ + "Infrastructure", + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "devices" + }, + { + "key": "columns", + "value": "objid,name,parentid,probe,group,host,status,active,tags,location,icon,totalsens,upsens,downsens,warnsens,pausedsens" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "devices", + "paging": { + "mode": "none" + } + }, + "matches": "none", + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Device", + "shape": "string", + "role": "label" + }, + { + "name": "parentid", + "displayName": "Parent ID", + "shape": "string", + "visible": false + }, + { + "name": "probe", + "displayName": "Probe", + "shape": "string" + }, + { + "name": "group", + "displayName": "Group", + "shape": "string" + }, + { + "name": "host", + "displayName": "Host", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "totalsens_raw", + "displayName": "Total Sensors", + "shape": "number", + "role": "value" + }, + { + "name": "upsens_raw", + "displayName": "Up Sensors", + "shape": "number" + }, + { + "name": "downsens_raw", + "displayName": "Down Sensors", + "shape": "number" + }, + { + "name": "warnsens_raw", + "displayName": "Warning Sensors", + "shape": "number" + }, + { + "name": "pausedsens_raw", + "displayName": "Paused Sensors", + "shape": "number" + }, + { + "name": "location_raw", + "displayName": "Location", + "shape": "string" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": "string" + }, + { + "name": "icon", + "displayName": "Icon", + "shape": "string", + "visible": false + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/groups.json b/plugins/PRTG/v1/dataStreams/groups.json new file mode 100644 index 00000000..2a4f10c7 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/groups.json @@ -0,0 +1,159 @@ +{ + "name": "groups", + "displayName": "Groups", + "description": "All PRTG groups with their status and sensor counts by state", + "tags": [ + "Infrastructure", + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "groups" + }, + { + "key": "filter_objid", + "value": "@neq(0)" + }, + { + "key": "columns", + "value": "objid,name,parentid,probe,status,active,totalsens,upsens,downsens,warnsens,pausedsens,location,tags" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "groups", + "paging": { + "mode": "none" + } + }, + "matches": "none", + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Group", + "shape": "string", + "role": "label" + }, + { + "name": "parentid", + "displayName": "Parent ID", + "shape": "string", + "visible": false + }, + { + "name": "probe", + "displayName": "Probe", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "totalsens_raw", + "displayName": "Total Sensors", + "shape": "number", + "role": "value" + }, + { + "name": "upsens_raw", + "displayName": "Up Sensors", + "shape": "number" + }, + { + "name": "downsens_raw", + "displayName": "Down Sensors", + "shape": "number" + }, + { + "name": "warnsens_raw", + "displayName": "Warning Sensors", + "shape": "number" + }, + { + "name": "pausedsens_raw", + "displayName": "Paused Sensors", + "shape": "number" + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + }, + { + "name": "location_raw", + "displayName": "Location", + "shape": "string" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/logs.json b/plugins/PRTG/v1/dataStreams/logs.json new file mode 100644 index 00000000..0eddc708 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/logs.json @@ -0,0 +1,91 @@ +{ + "name": "logs", + "displayName": "Log", + "description": "PRTG log entries with the object they relate to and the event that was recorded", + "tags": [ + "Events", + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "messages" + }, + { + "key": "columns", + "value": "objid,datetime,parent,type,name,status,message" + }, + { + "key": "filter_dstart", + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.start); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + }, + { + "key": "filter_dend", + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.end); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + }, + { + "key": "count", + "value": "5000" + } + ], + "pathToData": "messages", + "paging": { + "mode": "none" + } + }, + "matches": "none", + "metadata": [ + { + "name": "datetime_raw", + "displayName": "Time (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "time", + "displayName": "Time", + "computed": true, + "shape": "date", + "role": "timestamp", + "valueExpression": "{{ Number($['datetime_raw']) > 0 ? new Date((Number($['datetime_raw']) - 25569) * 86400000).toISOString() : null }}" + }, + { + "name": "objid", + "displayName": "Object ID", + "shape": "string", + "visible": false + }, + { + "name": "name", + "displayName": "Object", + "shape": "string", + "role": "label" + }, + { + "name": "parent", + "displayName": "Parent", + "shape": "string" + }, + { + "name": "type", + "displayName": "Type", + "shape": "string" + }, + { + "name": "status", + "displayName": "Event", + "shape": "string" + }, + { + "name": "message_raw", + "displayName": "Message", + "shape": "string" + } + ], + "timeframes": true, + "defaultTimeframe": "dashboard" +} diff --git a/plugins/PRTG/v1/dataStreams/probes.json b/plugins/PRTG/v1/dataStreams/probes.json new file mode 100644 index 00000000..ce6c5b31 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/probes.json @@ -0,0 +1,122 @@ +{ + "name": "probes", + "displayName": "Probes", + "description": "All PRTG probes with their status and connection state", + "tags": [ + "Infrastructure", + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "probes" + }, + { + "key": "filter_type", + "value": "probenode" + }, + { + "key": "columns", + "value": "objid,name,status,condition,active,location" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "probes", + "paging": { + "mode": "none" + } + }, + "matches": "none", + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Probe", + "shape": "string", + "role": "label" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "condition", + "displayName": "Connection", + "shape": "string" + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + }, + { + "name": "location_raw", + "displayName": "Location", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js new file mode 100644 index 00000000..c7905748 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js @@ -0,0 +1,81 @@ +// PRTG returns historic data pivoted: each sensor channel becomes its own +// dynamically-named column, so the column set differs per sensor (a disk sensor +// yields "Free Space C:", a ping sensor "Response Time"). A declared-column data +// stream cannot express that, so unpivot to one row per channel per interval. +// +// Timestamps need care. Unlike table.json's `*_raw` columns — which are OLE +// dates in UTC — historicdata.json only ever returns `datetime` as a string in +// the PRTG server's *local* time, with no raw/epoch equivalent. Emitting that +// as-is would put this stream an hour (or more) away from `Last Check` and the +// Log stream on the same dashboard, so convert local -> UTC here using the +// configured zone, which is interpolated into this script at request time. +const TIME_ZONE = + '{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; return tz || "UTC"; })() }}'; + +// Offset (ms) that `zone` was running at the given instant. Reading an instant +// as a wall clock and re-parsing it as UTC yields exactly that offset. +const offsetAt = (instant, zone) => { + const wall = new Date(instant).toLocaleString('sv-SE', { timeZone: zone }); + return Date.parse(wall.replace(' ', 'T') + 'Z') - instant; +}; + +// Interpret a naive wall-clock string as a time in `zone` and return real UTC. +// Applied twice so a reading that lands near a DST transition resolves against +// the offset actually in force rather than the one on the other side of it. +const wallClockToUtc = (naive, zone) => { + let guess = naive - offsetAt(naive, zone); + guess = naive - offsetAt(guess, zone); + return guess; +}; + +// `datetime` is either a single stamp ("8/20/2026 9:04:15 PM", when avg=0) or a +// bucket range ("8/20/2026 9:00:00 PM - 9:05:00 PM"). Take the bucket start. +const parseWhen = (raw) => { + const text = String(raw || ''); + const start = text.includes(' - ') ? text.split(' - ')[0].trim() : text.trim(); + const naive = new Date(start).getTime(); + if (isNaN(naive)) return null; + + let utc = naive; + try { + utc = wallClockToUtc(naive, TIME_ZONE); + } catch (e) { + // Unrecognised zone — fall back to treating the wall clock as UTC, + // matching the request side's own fallback. + utc = naive; + } + return new Date(utc).toISOString(); +}; + +// "100 %" -> 100 +const parseCoverage = (raw) => { + const num = parseFloat(String(raw || '').replace('%', '').trim()); + return isNaN(num) ? null : num; +}; + +// historicdata.json answers HTTP 200 with the bare text "Not enough monitoring +// data" when the window holds no retained readings, so `data` is not always an object. +const rows = (data && typeof data === 'object' && data.histdata) || []; +const out = []; + +for (const row of rows) { + const timestamp = parseWhen(row.datetime); + if (!timestamp) continue; + + const coverage = parseCoverage(row.coverage); + + for (const [channel, raw] of Object.entries(row)) { + if (channel === 'datetime' || channel === 'coverage') continue; + + // Intervals with no coverage come back as empty strings — drop them so + // they leave a genuine gap in the chart rather than plotting as zero. + if (raw === '' || raw === null || raw === undefined) continue; + + const value = Number(raw); + if (isNaN(value)) continue; + + out.push({ timestamp, channel, value, coverage }); + } +} + +result = out; diff --git a/plugins/PRTG/v1/dataStreams/sensorChannels.json b/plugins/PRTG/v1/dataStreams/sensorChannels.json new file mode 100644 index 00000000..59a03b4b --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/sensorChannels.json @@ -0,0 +1,66 @@ +{ + "name": "sensorChannels", + "displayName": "Sensor Channels", + "description": "Current, minimum and maximum reading for each channel of a sensor", + "tags": ["Monitoring", "Performance"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { "key": "content", "value": "channels" }, + { "key": "id", "value": "{{object.rawId}}" }, + { + "key": "columns", + "value": "objid,name,lastvalue,minimum,maximum" + }, + { "key": "count", "value": "50000" } + ], + "pathToData": "channels", + "paging": { "mode": "none" } + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["PRTG Sensor"] } }, + "metadata": [ + { + "name": "objid", + "displayName": "Channel ID", + "shape": "string", + "visible": false + }, + { + "name": "name", + "displayName": "Channel", + "shape": "string", + "role": "label" + }, + { + "name": "lastvalue_raw", + "displayName": "Last Value (Numeric)", + "shape": "number", + "role": "value" + }, + { + "name": "lastvalue", + "shape": "string", + "visible": false + }, + { + "name": "lastValueFormatted", + "displayName": "Last Value", + "computed": true, + "shape": "string", + "valueExpression": "{{ String($['lastvalue'] === null || $['lastvalue'] === undefined ? '' : $['lastvalue']).replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/ /g, ' ') }}" + }, + { + "name": "minimum_raw", + "displayName": "Minimum", + "shape": "number" + }, + { + "name": "maximum_raw", + "displayName": "Maximum", + "shape": "number" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/sensorHistory.json b/plugins/PRTG/v1/dataStreams/sensorHistory.json new file mode 100644 index 00000000..365e78d0 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/sensorHistory.json @@ -0,0 +1,112 @@ +{ + "name": "sensorHistory", + "displayName": "Sensor History", + "description": "Historic channel readings for a sensor, one row per channel per interval", + "tags": [ + "Monitoring", + "Performance" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "historicdata.json", + "getArgs": [ + { + "key": "id", + "value": "{{object.rawId}}" + }, + { + "key": "usecaption", + "value": "1" + }, + { + "key": "sdate", + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.start); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + }, + { + "key": "edate", + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.end); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + }, + { + "key": "avg", + "value": "{{ average || (new Date(timeframe.end) - new Date(timeframe.start) > 604800000 ? '3600' : null) }}" + } + ], + "postRequestScript": "sensorHistory.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "PRTG Sensor" + ] + } + }, + "ui": [ + { + "type": "choiceChips", + "name": "average", + "label": "Averaging interval", + "help": "How PRTG aggregates the readings. Left unset, PRTG uses 5-minute buckets, or hourly on ranges longer than seven days. **Raw** returns every individual reading — only practical over short timeframes.", + "options": [ + { + "value": "0", + "label": "Raw" + }, + { + "value": "300", + "label": "5 minutes" + }, + { + "value": "3600", + "label": "1 hour" + }, + { + "value": "86400", + "label": "1 day" + } + ], + "tileEditorStep": [ + "Timeframe" + ] + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "channel", + "displayName": "Channel", + "shape": "string", + "role": "label" + }, + { + "name": "value", + "displayName": "Value", + "shape": "number", + "role": "value" + }, + { + "name": "coverage", + "displayName": "Coverage", + "shape": [ + "percent", + { + "decimalPlaces": 0 + } + ] + } + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days" + ], + "defaultTimeframe": "dashboard" +} diff --git a/plugins/PRTG/v1/dataStreams/sensors.json b/plugins/PRTG/v1/dataStreams/sensors.json new file mode 100644 index 00000000..0fe64f1b --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/sensors.json @@ -0,0 +1,219 @@ +{ + "name": "sensors", + "displayName": "Sensors", + "description": "All PRTG sensors with their status, last reading, uptime and scanning interval", + "tags": [ + "Monitoring", + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "table.json", + "getArgs": [ + { + "key": "content", + "value": "sensors" + }, + { + "key": "columns", + "value": "objid,name,parentid,probe,group,device,type,status,message,lastvalue,lastcheck,interval,priority,active,tags,uptime,downtime" + }, + { + "key": "sortby", + "value": "objid" + }, + { + "key": "count", + "value": "50000" + } + ], + "pathToData": "sensors", + "paging": { + "mode": "none" + } + }, + "matches": "none", + "metadata": [ + { + "name": "objid", + "displayName": "ID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Sensor", + "shape": "string", + "role": "label" + }, + { + "name": "parentid", + "displayName": "Device ID", + "shape": "string", + "visible": false + }, + { + "name": "device", + "displayName": "Device", + "shape": "string" + }, + { + "name": "group", + "displayName": "Group", + "shape": "string" + }, + { + "name": "probe", + "displayName": "Probe", + "shape": "string" + }, + { + "name": "type", + "displayName": "Sensor Type", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": [ + "Up" + ], + "warning": [ + "Warning", + "Unusual" + ], + "error": [ + "Down", + "Down (Partial)", + "Down (Acknowledged)", + "No Probe", + "Not Connected" + ], + "unknown": [ + "Unknown", + "None", + "Scanning" + ], + "unmonitored": [ + "Paused", + "Paused by User", + "Paused by Dependency", + "Paused by Schedule", + "Paused by License", + "Paused until" + ] + } + } + ] + }, + { + "name": "status_raw", + "displayName": "Status Code", + "shape": "number", + "visible": false + }, + { + "name": "statusText", + "displayName": "Status Text", + "computed": true, + "shape": "string", + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + }, + { + "name": "message_raw", + "displayName": "Message", + "shape": "string" + }, + { + "name": "lastvalue", + "displayName": "Last Value", + "shape": "string" + }, + { + "name": "lastvalue_raw", + "displayName": "Last Value (Numeric)", + "shape": "number" + }, + { + "name": "lastcheck_raw", + "displayName": "Last Check (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "lastCheck", + "displayName": "Last Check", + "computed": true, + "shape": "date", + "valueExpression": "{{ Number($['lastcheck_raw']) > 0 ? new Date((Number($['lastcheck_raw']) - 25569) * 86400000).toISOString() : null }}" + }, + { + "name": "uptime_raw", + "displayName": "Uptime (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "uptimePercent", + "displayName": "Uptime", + "computed": true, + "role": "value", + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ], + "valueExpression": "{{ $['uptime_raw'] === '' || $['uptime_raw'] === null || $['uptime_raw'] === undefined ? null : Number($['uptime_raw']) / 10000 }}" + }, + { + "name": "downtime_raw", + "displayName": "Downtime (Raw)", + "shape": "number", + "visible": false + }, + { + "name": "downtimePercent", + "displayName": "Downtime", + "computed": true, + "shape": [ + "percent", + { + "decimalPlaces": 2 + } + ], + "valueExpression": "{{ $['downtime_raw'] === '' || $['downtime_raw'] === null || $['downtime_raw'] === undefined ? null : Number($['downtime_raw']) / 10000 }}" + }, + { + "name": "interval_raw", + "displayName": "Scanning Interval", + "shape": "seconds" + }, + { + "name": "priority_raw", + "displayName": "Priority", + "shape": "number" + }, + { + "name": "tags", + "displayName": "Tags", + "shape": "string" + }, + { + "name": "active", + "displayName": "Monitoring Active", + "shape": "boolean" + }, + { + "sourceId": "parentid", + "sourceType": "PRTG Device", + "name": "device" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/dataStreams/systemStatus.json b/plugins/PRTG/v1/dataStreams/systemStatus.json new file mode 100644 index 00000000..9c8722f9 --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/systemStatus.json @@ -0,0 +1,124 @@ +{ + "name": "systemStatus", + "displayName": "System Status", + "description": "Sensor counts by state, plus PRTG server version and edition", + "tags": ["Status", "Health"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "getstatus.htm", + "getArgs": [{ "key": "id", "value": "0" }] + }, + "matches": "none", + "metadata": [ + { "name": "UpSens", "shape": "string", "visible": false }, + { "name": "WarnSens", "shape": "string", "visible": false }, + { "name": "Alarms", "shape": "string", "visible": false }, + { "name": "PartialAlarms", "shape": "string", "visible": false }, + { "name": "AckAlarms", "shape": "string", "visible": false }, + { "name": "UnusualSens", "shape": "string", "visible": false }, + { "name": "PausedSens", "shape": "string", "visible": false }, + { "name": "UnknownSens", "shape": "string", "visible": false }, + { "name": "NewMessages", "shape": "string", "visible": false }, + { "name": "NewAlarms", "shape": "string", "visible": false }, + { + "name": "upSensors", + "displayName": "Up", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['UpSens']) || 0 }}" + }, + { + "name": "warningSensors", + "displayName": "Warning", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['WarnSens']) || 0 }}" + }, + { + "name": "downSensors", + "displayName": "Down", + "computed": true, + "shape": "number", + "role": "value", + "valueExpression": "{{ Number($['Alarms']) || 0 }}" + }, + { + "name": "downPartialSensors", + "displayName": "Down (Partial)", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['PartialAlarms']) || 0 }}" + }, + { + "name": "downAcknowledgedSensors", + "displayName": "Down (Acknowledged)", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['AckAlarms']) || 0 }}" + }, + { + "name": "unusualSensors", + "displayName": "Unusual", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['UnusualSens']) || 0 }}" + }, + { + "name": "pausedSensors", + "displayName": "Paused", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['PausedSens']) || 0 }}" + }, + { + "name": "unknownSensors", + "displayName": "Unknown", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['UnknownSens']) || 0 }}" + }, + { + "name": "totalSensors", + "displayName": "Total Sensors", + "computed": true, + "shape": "number", + "valueExpression": "{{ ['UpSens','WarnSens','Alarms','PartialAlarms','AckAlarms','UnusualSens','PausedSens','UnknownSens'].reduce((t, k) => t + (Number($[k]) || 0), 0) }}" + }, + { + "name": "newLogEntries", + "displayName": "New Log Entries", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['NewMessages']) || 0 }}" + }, + { + "name": "newAlarms", + "displayName": "New Alarms", + "computed": true, + "shape": "number", + "valueExpression": "{{ Number($['NewAlarms']) || 0 }}" + }, + { + "name": "Version", + "displayName": "PRTG Version", + "shape": "string" + }, + { + "name": "EditionType", + "displayName": "Edition", + "shape": "string" + }, + { + "name": "Clock", + "displayName": "Server Time", + "shape": "string" + }, + { + "name": "UserTimeZone", + "displayName": "Server Time Zone", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/defaultContent/device.dash.json b/plugins/PRTG/v1/defaultContent/device.dash.json new file mode 100644 index 00000000..20b8801b --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/device.dash.json @@ -0,0 +1,234 @@ +{ + "name": "Device", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Device]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "1bb12b4c-8810-483b-8edd-51b352c6b16d", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Device Details", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "345f14f6-a2ea-4900-a118-0e8b88b0d4c9", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "priority_raw", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "device", + "type", + "lastvalue", + "message_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "lastcheck_raw", + "uptime_raw", + "downtime_raw", + "lastvalue_raw", + "tags", + "active", + "probe" + ] + } + } + } + } + }, + { + "i": "62a62764-7448-46ca-b9de-8ca7990eb372", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors by Status", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "statusText", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "statusText_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "8764365c-0d8d-4629-b2f5-ea00fa313cc1", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensor Health", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "status", + "sublabel": "lastvalue", + "linkColumn": "none", + "columns": 3 + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/defaultContent/group.dash.json b/plugins/PRTG/v1/defaultContent/group.dash.json new file mode 100644 index 00000000..3b8c35bd --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/group.dash.json @@ -0,0 +1,270 @@ +{ + "name": "Group", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Group]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "7496a666-a94e-471e-a517-0f3279d891a7", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Group Details", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Group]}}" + }, + "variables": [ + "{{variables.[Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "1ea390a6-e8b4-47b4-878b-b80010da653b", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerDevices]}}", + "name": "containerDevices", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "totalsens_raw", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Group]}}" + }, + "variables": [ + "{{variables.[Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "host", + "group", + "totalsens_raw", + "downsens_raw", + "warnsens_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "icon", + "tags", + "active", + "location_raw", + "upsens_raw", + "pausedsens_raw" + ] + } + } + } + } + }, + { + "i": "f1f82fea-9132-4b2d-a7a0-c2cd46534c4f", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors by Status", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "statusText", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Group]}}" + }, + "variables": [ + "{{variables.[Group]}}" + ], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "statusText_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "2a6c2660-c49f-4fb3-82d6-1033fdbdf26a", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors Needing Attention", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "priority_raw", + "desc" + ] + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "statusText", + "operation": "notequals", + "value": "Up" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Group]}}" + }, + "variables": [ + "{{variables.[Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "device", + "type", + "lastvalue", + "message_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "lastcheck_raw", + "uptime_raw", + "downtime_raw", + "lastvalue_raw", + "tags", + "active", + "probe" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/defaultContent/manifest.json b/plugins/PRTG/v1/defaultContent/manifest.json new file mode 100644 index 00000000..e324425b --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/manifest.json @@ -0,0 +1,28 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "sites", + "type": "dashboard" + }, + { + "name": "probe", + "type": "dashboard" + }, + { + "name": "group", + "type": "dashboard" + }, + { + "name": "device", + "type": "dashboard" + }, + { + "name": "sensor", + "type": "dashboard" + } + ] +} diff --git a/plugins/PRTG/v1/defaultContent/overview.dash.json b/plugins/PRTG/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..9396c17a --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/overview.dash.json @@ -0,0 +1,381 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "10446ece-2e09-46a4-8557-104d33b6c192", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Total Sensors", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[systemStatus]}}", + "name": "systemStatus", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "totalSensors", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "b109556e-40d9-4a7f-9a61-d94c1ce08d6c", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Up", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[systemStatus]}}", + "name": "systemStatus", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "upSensors", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "d6ce20d0-4ad5-44a4-a675-b676dd9e9bce", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Warning", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[systemStatus]}}", + "name": "systemStatus", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "warningSensors", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "cc6eab65-7a26-4914-8c6d-83c1a17190a8", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Down", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[systemStatus]}}", + "name": "systemStatus", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "downSensors", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "a2dd7f2b-6ae1-44c1-ba24-b9c5e438bac1", + "x": 0, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors by Status", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[sensors]}}", + "name": "sensors", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["statusText", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + }, + "sort": { "by": [["count", "desc"]] } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "statusText_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "40eda2eb-edf2-49da-882d-c6ded2718dbd", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors Needing Attention", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[sensors]}}", + "name": "sensors", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "statusText", + "operation": "notequals", + "value": "Up" + } + ] + }, + "sort": { "by": [["priority_raw", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "device", + "message_raw", + "lastvalue", + "priority_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "lastcheck_raw", + "uptime_raw", + "downtime_raw", + "lastvalue_raw", + "tags", + "active", + "group", + "probe" + ] + } + } + } + } + }, + { + "i": "c5bcf1af-e267-49eb-bad3-74a01357a5da", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["totalsens_raw", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "host", + "group", + "totalsens_raw", + "downsens_raw", + "warnsens_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "icon", + "tags", + "active", + "probe", + "location_raw", + "upsens_raw", + "pausedsens_raw" + ] + } + } + } + } + }, + { + "i": "3df87dd0-1393-464d-b1ba-ec4d57d8163b", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors per Device", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[sensors]}}", + "name": "sensors", + "pluginConfigId": "{{configId}}", + "group": { + "by": [["device", "uniqueValues"]], + "aggregate": [{ "type": "count" }] + }, + "sort": { "by": [["count", "desc"]], "top": 10 } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "device_uniqueValues", + "yAxisData": ["count"], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Sensors", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { "type": "auto" } + } + } + } + } + }, + { + "i": "1ed6f225-3d35-4b94-be18-7571d1517df0", + "x": 0, + "y": 8, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Log Entries", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[logs]}}", + "name": "logs", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["time", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "time", + "name", + "status", + "message_raw" + ], + "hiddenColumns": ["objid", "datetime_raw"] + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/defaultContent/probe.dash.json b/plugins/PRTG/v1/defaultContent/probe.dash.json new file mode 100644 index 00000000..1cd9aa3d --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/probe.dash.json @@ -0,0 +1,270 @@ +{ + "name": "Probe", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Probe]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "baa06389-4177-4be5-8250-705039ec0be9", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Probe Details", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Probes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Probe]}}" + }, + "variables": [ + "{{variables.[Probe]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "07606a01-c88f-4ceb-947c-c4a8f99cd53b", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerDevices]}}", + "name": "containerDevices", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "totalsens_raw", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Probes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Probe]}}" + }, + "variables": [ + "{{variables.[Probe]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "host", + "group", + "totalsens_raw", + "downsens_raw", + "warnsens_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "icon", + "tags", + "active", + "location_raw", + "upsens_raw", + "pausedsens_raw" + ] + } + } + } + } + }, + { + "i": "2357762b-9473-4786-b33f-5aa153f4c5b8", + "x": 0, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors by Status", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "statusText", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Probes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Probe]}}" + }, + "variables": [ + "{{variables.[Probe]}}" + ], + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "statusText_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "69095342-c57d-45b7-b878-801fef0bb329", + "x": 2, + "y": 4, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors Needing Attention", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[containerSensors]}}", + "name": "containerSensors", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "priority_raw", + "desc" + ] + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "statusText", + "operation": "notequals", + "value": "Up" + } + ] + } + }, + "scope": { + "scope": "{{scopes.[Probes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Probe]}}" + }, + "variables": [ + "{{variables.[Probe]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "name", + "device", + "type", + "lastvalue", + "message_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "lastcheck_raw", + "uptime_raw", + "downtime_raw", + "lastvalue_raw", + "tags", + "active", + "probe" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/defaultContent/sensor.dash.json b/plugins/PRTG/v1/defaultContent/sensor.dash.json new file mode 100644 index 00000000..dc9129fd --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/sensor.dash.json @@ -0,0 +1,161 @@ +{ + "name": "Sensor", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Sensor]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "cbf63d04-ab31-4719-9d12-4508c228d203", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensor Details", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "106790b9-2dbb-4c15-809f-fa7301be4eaa", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Channels", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[sensorChannels]}}", + "name": "sensorChannels", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "lastValueFormatted", + "lastvalue_raw", + "minimum_raw", + "maximum_raw" + ], + "hiddenColumns": [ + "objid", + "lastvalue" + ] + } + } + } + } + }, + { + "i": "cd81bb9b-815f-4e70-95d0-6fdff5a3d1f2", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Channel History", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[sensorHistory]}}", + "name": "sensorHistory", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "timestamp", + "asc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "channel", + "showLegend": true, + "legendPosition": "bottom", + "yAxisLabel": "", + "showYAxisLabel": false, + "showTrendLine": false + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/defaultContent/sites.dash.json b/plugins/PRTG/v1/defaultContent/sites.dash.json new file mode 100644 index 00000000..9bf81382 --- /dev/null +++ b/plugins/PRTG/v1/defaultContent/sites.dash.json @@ -0,0 +1,374 @@ +{ + "name": "Sites", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "52e399da-f733-4c80-977c-2a954fb8a570", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sites", + "description": "Distinct locations set in PRTG", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "location_raw", + "operation": "notempty" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "distinctCount", + "names": [ + "location_raw" + ] + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "location_raw_distinctCount", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "4b14f0c1-58ca-45fb-8852-ff3e88e89997", + "x": 1, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices per Site", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "location_raw", + "operation": "notempty" + } + ] + }, + "group": { + "by": [ + [ + "location_raw", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ], + "top": 15 + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "location_raw_uniqueValues", + "yAxisData": [ + "count" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Devices", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "cf8d29bf-32d9-4a07-919d-5a47bab1278c", + "x": 0, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors Down by Site", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "location_raw", + "operation": "notempty" + } + ] + }, + "group": { + "by": [ + [ + "location_raw", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "downsens_raw" + ] + } + ] + }, + "sort": { + "by": [ + [ + "downsens_raw_sum", + "desc" + ] + ], + "top": 15 + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "location_raw_uniqueValues", + "yAxisData": [ + "downsens_raw_sum" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Sensors down", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": true, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "ce89869f-2e91-417d-97ea-8f07133ff836", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors by Site", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "location_raw", + "operation": "notempty" + } + ] + }, + "group": { + "by": [ + [ + "location_raw", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "sum", + "names": [ + "totalsens_raw" + ] + } + ] + }, + "sort": { + "by": [ + [ + "totalsens_raw_sum", + "desc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "totalsens_raw_sum", + "labelColumn": "location_raw_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "da8413f2-9466-46fe-9735-47584da96ed0", + "x": 0, + "y": 5, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices by Site", + "description": "Devices with a location set in PRTG", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "location_raw", + "operation": "notempty" + } + ] + }, + "sort": { + "by": [ + [ + "location_raw", + "asc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "status", + "location_raw", + "name", + "host", + "group", + "probe", + "totalsens_raw", + "downsens_raw", + "warnsens_raw" + ], + "hiddenColumns": [ + "objid", + "parentid", + "status_raw", + "statusText", + "status[Expanded].rawState", + "icon", + "tags", + "active", + "upsens_raw", + "pausedsens_raw" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/PRTG/v1/docs/README.md b/plugins/PRTG/v1/docs/README.md new file mode 100644 index 00000000..b0b6e9ab --- /dev/null +++ b/plugins/PRTG/v1/docs/README.md @@ -0,0 +1,133 @@ +Monitor your [PRTG Network Monitor](https://www.paessler.com/prtg) installation in SquaredUp — probes, groups, +devices and sensors, with current status, channel readings, historic sensor data and the PRTG log — via the +[PRTG HTTP API](https://www.paessler.com/manuals/prtg/http_api). + +> ⚠️ This plugin uses the **PRTG API v1** (`/api/table.json`). It does not use PRTG API v2, whose object +> endpoints are still marked experimental by Paessler. Any PRTG version that supports API keys will work, +> including PRTG Hosted Monitor, PRTG Network Monitor and PRTG Enterprise Monitor. + +## Setup + +You will need the **URL** of your PRTG server and a PRTG **API key**. + +1. Sign in to your PRTG web interface as a user allowed to create API keys. +2. Go to **Setup → Account Settings → [API Keys](https://www.paessler.com/manuals/prtg/api_keys)**. +3. Hover over the **Add** button and choose **Add API Key**. +4. Set the token type to **Scripting** — the **Desktop** type is reserved for PRTG MultiBoard, and only one + is allowed per account. +5. Give the key **Read access**. The plugin only ever reads, so `Acknowledge`, `Write` and `Full` access + are unnecessary. +6. Click **OK**, then copy the generated key immediately — PRTG will not show it again. If you lose it, + delete the key and create a new one. +7. Paste the key into the **API key** field, and your PRTG address into **PRTG URL**. +8. Set **PRTG time zone** to the time zone of the account whose key you just created — it is shown in PRTG + under **Setup → Account Settings → My Account → Time Zone**. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- | +| **PRTG URL** | The base address of your PRTG web interface — for example `https://prtg.example.com` or `https://yourname.my-prtg.com`. Include the sub-path if PRTG sits behind a reverse proxy, but no query string. | The address bar of your PRTG web interface. | Yes | +| **API key** | Authenticates every request. Sent as the `apitoken` query parameter, which is the only scheme PRTG's v1 API accepts. | PRTG → **Setup → Account Settings → API Keys**. | Yes | +| **PRTG time zone** | The time zone of the PRTG account whose API key you supplied, as an IANA name such as `Europe/London`. Only affects **Sensor History** and **Log**. Defaults to UTC. | PRTG → **Setup → Account Settings → My Account → Time Zone**. | No | +| **Ignore certificate errors** | Skips TLS certificate validation. Only enable for an on-premise PRTG server using a self-signed certificate. | — | No | + +On save, the plugin calls PRTG's status endpoint to confirm the URL and key. A failure means the URL is +unreachable or the key is invalid, expired, or deleted. + +> ⚠️ **Get the time zone right.** PRTG interprets date ranges in its *own* time zone rather than UTC, so the +> wrong zone shifts **Sensor History** and **Log** — and on short timeframes can make them look empty. +> Everything else is unaffected. Note that PRTG labels a zone by its *standard* offset, so a UK server shown +> as "(UTC+00:00) … London" is really running an hour ahead during British Summer Time — pick +> `Europe/London` rather than `UTC` and daylight saving is handled for you. + +## What this plugin monitors + +- **Estate health at a glance** — how many sensors are up, warning, down, paused, unusual or unknown across + the whole installation, plus your PRTG version and edition. +- **The PRTG hierarchy** — probes, groups, devices and sensors are all indexed, so you can search for them, + scope dashboards to them, and use them as dashboard variables. +- **Device and sensor state** — per-device sensor counts by state and host address; per-sensor status, last + reading, status message, uptime and downtime percentage, scanning interval and priority. +- **Sensor detail over time** — the current, minimum and maximum reading of every channel on a sensor, and + historic readings for any timeframe. +- **The PRTG log** — what PRTG recorded, against which object, over the dashboard's timeframe. +- **Sites** — where a **Location** is set in PRTG, devices and their sensor counts are broken down by it, so + you can compare sites side by side. + +The out-of-the-box dashboards include an estate-wide **Overview**, a **Sites** breakdown, and a perspective +for each **Probe**, **Group**, **Device** and **Sensor**. + +## Data streams + +- **System Status** — installation-wide sensor counts by state, plus PRTG version, edition and server time. Account-wide. +- **Probes** — every probe with its status and connection state. Account-wide. +- **Groups** — every group with its status and sensor counts by state. Account-wide. +- **Devices** — every device with its status, host address and sensor counts by state. Account-wide. +- **Sensors** — every sensor with its status, last reading, uptime and scanning interval. Account-wide. +- **Sensors in Probe, Group or Device** — the same sensor detail, restricted to everything beneath one probe, group or device. Per-object. +- **Devices in Probe or Group** — device detail restricted to everything beneath one probe or group. Per-object. +- **Sensor Channels** — the current, minimum and maximum reading for each channel of one sensor. Per-object. +- **Sensor History** — historic channel readings for one sensor, one row per channel per interval, over the selected timeframe. Per-object. +- **Log** — PRTG log entries over the selected timeframe. Account-wide. + +## What gets indexed + +| Object type | API source | Represents | +| ---------------- | ------------------------------------------------------ | -------------------------------------------------------------- | +| **PRTG Probe** | `GET /api/table.json?content=probes&filter_type=probenode` | A local or remote probe that performs monitoring. | +| **PRTG Group** | `GET /api/table.json?content=groups` | A group of devices. Groups can nest inside other groups. | +| **PRTG Device** | `GET /api/table.json?content=devices` | A monitored host, identified by its address. | +| **PRTG Sensor** | `GET /api/table.json?content=sensors` | A single check running against a device. | + +**Relationships:** every object stores its PRTG parent's id as a `parentId` property, and sensors also store +`deviceId`, `deviceName`, `groupName` and `probeName`. The **Sensors** stream links its Device column +straight to the **PRTG Device** object, so you can click through from a sensor to the device it runs on. See +the first limitation below for why these are properties rather than graph relationships. + +**Sites:** PRTG has no "site" object. Probes, groups and devices each store a `location` property holding +PRTG's **Location** field, which is the closest thing — a remote probe usually maps to one site, and groups +are the usual way to organise by site. The **Sites** dashboard groups devices by `location`. + +## Known limitations + +- **No graph relationships between PRTG objects.** SquaredUp only supports creating graph edges from + code-based plugins, not low-code ones like this. The PRTG hierarchy is therefore expressed as properties + (`parentId`, `deviceName`, `groupName`, `probeName`) and through dashboard scoping and drilldown, rather + than as traversable parent/child links in the graph. +- **Date ranges follow PRTG's own time zone.** See the **PRTG time zone** field above. PRTG offers no way to + query in UTC and accepts no relative ranges, so the zone has to be supplied. It is also used to convert + **Sensor History** timestamps to UTC, because that endpoint reports times only as local text with no UTC + equivalent — so the wrong zone shifts both the range queried *and* the times plotted. An unrecognised zone + name falls back to UTC rather than failing the request. +- **Very large installations may hit a response size limit.** Each object type is fetched in a single + request rather than page by page. In practice the sensor import is the binding constraint and should + comfortably handle around 10,000 sensors, which is also Paessler's own recommended maximum per core + server. Larger installations may fail to import sensors. +- **Sensor History is limited to 30 days**, and beyond a week it is averaged hourly. Finer buckets over a + long range return more rows than the platform's response size limit allows, so when you have not chosen an + **Averaging interval** the plugin asks PRTG for hourly figures on ranges longer than seven days. Choosing + **5 minutes** or **Raw** explicitly on a long range can still exceed the limit and fail the tile. +- **The log returns at most 5,000 entries per query.** PRTG returns newest first, so on a busy installation + over a long timeframe the oldest entries in the range are dropped without warning. Unlike the object + tables, PRTG reports no usable total for the log, so there is no way to detect that truncation happened — + shorten the dashboard timeframe to see further back. +- **PRTG tags are a single string.** They are indexed as a `prtgTags` property holding PRTG's + comma-separated list, not as native SquaredUp tags. +- **Location is only reported where it is set, and never on sensors.** PRTG's API returns a `location` for a + probe, group or device only when one is set on that object directly — it is not inherited down the tree in + the API response, and sensors never carry one at all. The **Sites** dashboard therefore aggregates + *devices* (and PRTG's per-device sensor counts) by location, and shows nothing until you set **Location** + on your devices in PRTG. +- **No map visualisation.** SquaredUp has no map tile type, and PRTG's API exposes no usable coordinates — + `lat`/`lon` are not valid columns and the `lonlat` property reads `0,0` unless PRTG has geocoded the + location. Sites are therefore shown as charts and tables, not on a map. +- **Sensor status in the graph is not indexed.** Because objects are re-imported only every 12 hours by + default, indexing a sensor's status would show stale health. Use the **Sensors** stream for current status. +- **Historic data granularity is PRTG's.** PRTG aggregates history into buckets and returns a `coverage` + percentage per bucket; intervals PRTG has no data for are omitted rather than plotted as zero. Choosing + **Raw** on **Sensor History** is only practical over short timeframes. +- **The API key travels in the query string.** PRTG's v1 API rejects `Authorization: Bearer`, so the token + must be sent as the `apitoken` query parameter. Always use HTTPS. +- **Read-only.** The plugin never creates, modifies, acknowledges, pauses or deletes anything in PRTG, and a + **Read access** API key is all it needs. diff --git a/plugins/PRTG/v1/icon.svg b/plugins/PRTG/v1/icon.svg new file mode 100644 index 00000000..6cb211ed --- /dev/null +++ b/plugins/PRTG/v1/icon.svg @@ -0,0 +1,32 @@ + + + + + + diff --git a/plugins/PRTG/v1/indexDefinitions/default.json b/plugins/PRTG/v1/indexDefinitions/default.json new file mode 100644 index 00000000..a4c60383 --- /dev/null +++ b/plugins/PRTG/v1/indexDefinitions/default.json @@ -0,0 +1,131 @@ +{ + "steps": [ + { + "name": "probes", + "dataStream": { + "name": "probes" + }, + "timeframe": "none", + "objectMapping": { + "id": "objid", + "name": "name", + "type": { + "value": "PRTG Probe" + }, + "properties": [ + { + "connection": "condition" + }, + { + "location": "location_raw" + } + ] + } + }, + { + "name": "groups", + "dataStream": { + "name": "groups" + }, + "timeframe": "none", + "objectMapping": { + "id": "objid", + "name": "name", + "type": { + "value": "PRTG Group" + }, + "properties": [ + { + "parentId": "parentid" + }, + { + "probeName": "probe" + }, + { + "location": "location_raw" + }, + { + "prtgTags": "tags" + } + ] + } + }, + { + "name": "devices", + "dataStream": { + "name": "devices" + }, + "timeframe": "none", + "objectMapping": { + "id": "objid", + "name": "name", + "type": { + "value": "PRTG Device" + }, + "properties": [ + { + "parentId": "parentid" + }, + { + "host": "host" + }, + { + "probeName": "probe" + }, + { + "groupName": "group" + }, + { + "location": "location_raw" + }, + { + "prtgTags": "tags" + } + ] + } + }, + { + "name": "sensors", + "dataStream": { + "name": "sensors" + }, + "timeframe": "none", + "objectMapping": { + "id": "objid", + "name": "name", + "type": { + "value": "PRTG Sensor" + }, + "properties": [ + { + "parentId": "parentid" + }, + { + "deviceId": "parentid" + }, + { + "sensorType": "type" + }, + { + "deviceName": "device" + }, + { + "groupName": "group" + }, + { + "probeName": "probe" + }, + { + "scanningInterval": "interval_raw" + }, + { + "priority": "priority_raw" + }, + { + "prtgTags": "tags" + } + ] + } + } + ] +} diff --git a/plugins/PRTG/v1/metadata.json b/plugins/PRTG/v1/metadata.json new file mode 100644 index 00000000..fdde78b7 --- /dev/null +++ b/plugins/PRTG/v1/metadata.json @@ -0,0 +1,50 @@ +{ + "name": "prtg", + "displayName": "PRTG Network Monitor", + "version": "1.0.0", + "author": { "name": "@Deenk", "type": "community" }, + "description": "Monitor your PRTG installation — probes, groups, devices and sensors, with sensor status, channel readings and historic data.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "prtg", + "paessler", + "network", + "monitoring", + "sensors", + "snmp", + "infrastructure" + ], + "objectTypes": [ + "PRTG Probe", + "PRTG Group", + "PRTG Device", + "PRTG Sensor" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/PRTG/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/PRTG/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{host.endsWith('/') ? host.slice(0, -1) : host}}/api", + "authMode": "none", + "headers": [], + "queryArgs": [{ "key": "apitoken", "value": "{{apiToken}}" }], + "ignoreCertificateErrors": "{{typeof ignoreCertificateErrors !== 'undefined' && ignoreCertificateErrors === true}}" + } + } +} diff --git a/plugins/PRTG/v1/scopes.json b/plugins/PRTG/v1/scopes.json new file mode 100644 index 00000000..a3ad5767 --- /dev/null +++ b/plugins/PRTG/v1/scopes.json @@ -0,0 +1,50 @@ +[ + { + "name": "Probes", + "matches": { + "sourceType": { "type": "oneOf", "values": ["PRTG Probe"] } + }, + "variable": { + "name": "Probe", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Groups", + "matches": { + "sourceType": { "type": "oneOf", "values": ["PRTG Group"] } + }, + "variable": { + "name": "Group", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Devices", + "matches": { + "sourceType": { "type": "oneOf", "values": ["PRTG Device"] } + }, + "variable": { + "name": "Device", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Sensors", + "matches": { + "sourceType": { "type": "oneOf", "values": ["PRTG Sensor"] } + }, + "variable": { + "name": "Sensor", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/PRTG/v1/ui.json b/plugins/PRTG/v1/ui.json new file mode 100644 index 00000000..c0b32649 --- /dev/null +++ b/plugins/PRTG/v1/ui.json @@ -0,0 +1,80 @@ +[ + { + "name": "host", + "label": "PRTG URL", + "type": "url", + "help": "Base URL of your PRTG web interface. Include the port if PRTG does not run on the default one, and the path if PRTG sits behind a reverse proxy.", + "placeholder": "https://prtg.example.com", + "validation": { + "required": true, + "pattern": { + "value": "^https?://[^\\s/?#]+(/[^\\s?#]*)?$", + "message": "Enter the scheme and host, e.g. https://prtg.example.com — no query string" + } + } + }, + { + "name": "apiToken", + "label": "API key", + "type": "password", + "placeholder": "Enter the PRTG API key", + "help": "A PRTG API key with **Read access**, created under **Setup → Account Settings → API Keys** using the **Scripting** token type. PRTG only shows the key once, so copy it when you create it.", + "validation": { "required": true } + }, + { + "name": "serverTimeZone", + "label": "PRTG time zone", + "type": "autocomplete", + "allowCustomValues": true, + "isClearable": true, + "help": "The time zone of the PRTG **account** whose API key you entered above — PRTG interprets date ranges in that zone rather than UTC. Find it in PRTG under **Setup → Account Settings → My Account → Time Zone**. Daylight saving is handled automatically, so pick the region (for example `Europe/London`) rather than a fixed offset. Any [IANA time zone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) can be typed in. Defaults to UTC if left empty — the **System Status** data stream reports the zone PRTG is actually using.", + "data": { + "source": "fixed", + "values": [ + { "value": "UTC", "label": "UTC" }, + { "value": "Europe/London", "label": "Europe/London (UK)" }, + { "value": "Europe/Dublin", "label": "Europe/Dublin" }, + { "value": "Europe/Lisbon", "label": "Europe/Lisbon" }, + { "value": "Europe/Paris", "label": "Europe/Paris" }, + { "value": "Europe/Berlin", "label": "Europe/Berlin" }, + { "value": "Europe/Madrid", "label": "Europe/Madrid" }, + { "value": "Europe/Amsterdam", "label": "Europe/Amsterdam" }, + { "value": "Europe/Stockholm", "label": "Europe/Stockholm" }, + { "value": "Europe/Warsaw", "label": "Europe/Warsaw" }, + { "value": "Europe/Athens", "label": "Europe/Athens" }, + { "value": "Europe/Helsinki", "label": "Europe/Helsinki" }, + { "value": "Europe/Moscow", "label": "Europe/Moscow" }, + { "value": "America/New_York", "label": "America/New_York (US Eastern)" }, + { "value": "America/Chicago", "label": "America/Chicago (US Central)" }, + { "value": "America/Denver", "label": "America/Denver (US Mountain)" }, + { "value": "America/Phoenix", "label": "America/Phoenix (no DST)" }, + { "value": "America/Los_Angeles", "label": "America/Los_Angeles (US Pacific)" }, + { "value": "America/Toronto", "label": "America/Toronto" }, + { "value": "America/Vancouver", "label": "America/Vancouver" }, + { "value": "America/Mexico_City", "label": "America/Mexico_City" }, + { "value": "America/Sao_Paulo", "label": "America/Sao_Paulo" }, + { "value": "Africa/Lagos", "label": "Africa/Lagos" }, + { "value": "Africa/Cairo", "label": "Africa/Cairo" }, + { "value": "Africa/Johannesburg", "label": "Africa/Johannesburg" }, + { "value": "Asia/Dubai", "label": "Asia/Dubai" }, + { "value": "Asia/Kolkata", "label": "Asia/Kolkata (India)" }, + { "value": "Asia/Singapore", "label": "Asia/Singapore" }, + { "value": "Asia/Hong_Kong", "label": "Asia/Hong_Kong" }, + { "value": "Asia/Shanghai", "label": "Asia/Shanghai" }, + { "value": "Asia/Tokyo", "label": "Asia/Tokyo" }, + { "value": "Asia/Seoul", "label": "Asia/Seoul" }, + { "value": "Australia/Perth", "label": "Australia/Perth" }, + { "value": "Australia/Brisbane", "label": "Australia/Brisbane" }, + { "value": "Australia/Sydney", "label": "Australia/Sydney" }, + { "value": "Australia/Melbourne", "label": "Australia/Melbourne" }, + { "value": "Pacific/Auckland", "label": "Pacific/Auckland" } + ] + } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "help": "Enable when connecting to a PRTG server with a self-signed certificate." + } +] From 2ca1426ad87bf0c892fce872f837478b7452d69e Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 24 Aug 2026 12:24:31 +0100 Subject: [PATCH 2/7] Address CodeRabbit review on the PRTG plugin Correctness fixes: - Map PRTG status code 11 to "Not Licensed" across all six object streams, and drop "Paused by License" from the unmonitored state maps -- PRTG never returns that string. - Parse the historic-data `datetime` string explicitly instead of relying on `new Date()`, which rejects PRTG's European format ("04.12.2017 16:35:08") and resolves the others against the host time zone rather than PRTG's. - Report a genuine 0% coverage as 0 rather than null. Robustness: - Build the PRTG date strings from Intl.DateTimeFormat.formatToParts in sensorHistory.js, sensorHistory.json and logs.json, so nothing depends on how a given ICU build separates the date from the time. - Return null from offsetAt when the parts are not numeric, and propagate that, so `new Date(NaN).toISOString()` is unreachable. - Strip everything outside the IANA character set from the interpolated time zone, so a quote in a custom value cannot inject into the request script. - Clamp the Sensor History window to 30 days, so a tile following a longer dashboard timeframe returns the most recent 30 days instead of asking PRTG for a range it cannot serve within the response cap. Conventions: - Rename source types to unprefixed upstream terms (Probe, Group, Device, Sensor) per REVIEW.md. Display text is unchanged. - Move scopes.json to defaultContent/, where the other 30 plugins and the authoring docs put it. - Use a recursive CODEOWNERS pattern for the plugin directory. Documentation: - Reword the authentication notes. PRTG's manual documents `Authorization: Bearer`, but 26.3.122.1665 answers 401 "Unsupported authorization scheme" to that, to `Authorization: apitoken` and to `X-Api-Key`, leaving the apitoken query parameter as the only scheme that works. Verified against PRTG 26.3.122.1665 with the account time zone at UTC+1. `table.json` raw datetimes are confirmed UTC, so the log conversion is unchanged. `datetime_raw` is not usable in historicdata.json: it is only returned when `usecaption` is omitted, which collapses every channel into a single unnamed column, and it carries the bucket end rather than its start. Co-Authored-By: Claude Opus 5 (1M context) --- .github/CODEOWNERS | 2 +- plugins/PRTG/v1/custom_types.json | 16 +-- .../PRTG/v1/dataStreams/containerDevices.json | 8 +- .../PRTG/v1/dataStreams/containerSensors.json | 12 +- plugins/PRTG/v1/dataStreams/devices.json | 4 +- plugins/PRTG/v1/dataStreams/groups.json | 4 +- plugins/PRTG/v1/dataStreams/logs.json | 4 +- plugins/PRTG/v1/dataStreams/probes.json | 4 +- .../v1/dataStreams/scripts/sensorHistory.js | 111 ++++++++++++++---- .../PRTG/v1/dataStreams/sensorChannels.json | 2 +- .../PRTG/v1/dataStreams/sensorHistory.json | 6 +- plugins/PRTG/v1/dataStreams/sensors.json | 6 +- .../PRTG/v1/{ => defaultContent}/scopes.json | 8 +- plugins/PRTG/v1/docs/README.md | 21 ++-- plugins/PRTG/v1/indexDefinitions/default.json | 8 +- plugins/PRTG/v1/metadata.json | 8 +- 16 files changed, 143 insertions(+), 81 deletions(-) rename plugins/PRTG/v1/{ => defaultContent}/scopes.json (76%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 886cf3ae..224e6a31 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,7 +14,7 @@ plugins/Huntress/* @Deenk plugins/FantasyPremierLeague/* @TimWheeler-SQUP plugins/GoogleSheets/* @kieranlangton plugins/MetOffice/* @blackgrouse -plugins/PRTG/* @Deenk +/plugins/PRTG/ @Deenk plugins/Phare/* @vinbab plugins/Postcoder/* @richbenwell plugins/RDAP/* @richbenwell diff --git a/plugins/PRTG/v1/custom_types.json b/plugins/PRTG/v1/custom_types.json index 828e26ad..4b5f99a1 100644 --- a/plugins/PRTG/v1/custom_types.json +++ b/plugins/PRTG/v1/custom_types.json @@ -1,28 +1,28 @@ [ { - "name": "PRTG Probe", - "sourceType": "PRTG Probe", + "name": "Probe", + "sourceType": "Probe", "icon": "tower-broadcast", "singular": "Probe", "plural": "Probes" }, { - "name": "PRTG Group", - "sourceType": "PRTG Group", + "name": "Group", + "sourceType": "Group", "icon": "sitemap", "singular": "Group", "plural": "Groups" }, { - "name": "PRTG Device", - "sourceType": "PRTG Device", + "name": "Device", + "sourceType": "Device", "icon": "server", "singular": "Device", "plural": "Devices" }, { - "name": "PRTG Sensor", - "sourceType": "PRTG Sensor", + "name": "Sensor", + "sourceType": "Sensor", "icon": "gauge", "singular": "Sensor", "plural": "Sensors" diff --git a/plugins/PRTG/v1/dataStreams/containerDevices.json b/plugins/PRTG/v1/dataStreams/containerDevices.json index f2bf6b51..b706f90e 100644 --- a/plugins/PRTG/v1/dataStreams/containerDevices.json +++ b/plugins/PRTG/v1/dataStreams/containerDevices.json @@ -41,8 +41,8 @@ "sourceType": { "type": "oneOf", "values": [ - "PRTG Probe", - "PRTG Group" + "Probe", + "Group" ] } }, @@ -111,7 +111,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -129,7 +129,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "totalsens_raw", diff --git a/plugins/PRTG/v1/dataStreams/containerSensors.json b/plugins/PRTG/v1/dataStreams/containerSensors.json index ebfd05cf..a50f18ab 100644 --- a/plugins/PRTG/v1/dataStreams/containerSensors.json +++ b/plugins/PRTG/v1/dataStreams/containerSensors.json @@ -41,9 +41,9 @@ "sourceType": { "type": "oneOf", "values": [ - "PRTG Probe", - "PRTG Group", - "PRTG Device" + "Probe", + "Group", + "Device" ] } }, @@ -117,7 +117,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -135,7 +135,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "message_raw", @@ -224,7 +224,7 @@ }, { "sourceId": "parentid", - "sourceType": "PRTG Device", + "sourceType": "Device", "name": "device" } ], diff --git a/plugins/PRTG/v1/dataStreams/devices.json b/plugins/PRTG/v1/dataStreams/devices.json index 67f61485..a68d9d7f 100644 --- a/plugins/PRTG/v1/dataStreams/devices.json +++ b/plugins/PRTG/v1/dataStreams/devices.json @@ -99,7 +99,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -117,7 +117,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "totalsens_raw", diff --git a/plugins/PRTG/v1/dataStreams/groups.json b/plugins/PRTG/v1/dataStreams/groups.json index 2a4f10c7..b3dadcb9 100644 --- a/plugins/PRTG/v1/dataStreams/groups.json +++ b/plugins/PRTG/v1/dataStreams/groups.json @@ -93,7 +93,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -111,7 +111,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "totalsens_raw", diff --git a/plugins/PRTG/v1/dataStreams/logs.json b/plugins/PRTG/v1/dataStreams/logs.json index 0eddc708..7bf35ce3 100644 --- a/plugins/PRTG/v1/dataStreams/logs.json +++ b/plugins/PRTG/v1/dataStreams/logs.json @@ -21,11 +21,11 @@ }, { "key": "filter_dstart", - "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.start); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var fmt = function(zone){ var parts = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, hourCycle: \"h23\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }).formatToParts(new Date(timeframe.start)); var p = {}; for (var i = 0; i < parts.length; i++) { p[parts[i].type] = parts[i].value; } return p.year + \"-\" + p.month + \"-\" + p.day + \"-\" + p.hour + \"-\" + p.minute + \"-\" + p.second; }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" }, { "key": "filter_dend", - "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.end); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var fmt = function(zone){ var parts = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, hourCycle: \"h23\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }).formatToParts(new Date(timeframe.end)); var p = {}; for (var i = 0; i < parts.length; i++) { p[parts[i].type] = parts[i].value; } return p.year + \"-\" + p.month + \"-\" + p.day + \"-\" + p.hour + \"-\" + p.minute + \"-\" + p.second; }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" }, { "key": "count", diff --git a/plugins/PRTG/v1/dataStreams/probes.json b/plugins/PRTG/v1/dataStreams/probes.json index ce6c5b31..fce83668 100644 --- a/plugins/PRTG/v1/dataStreams/probes.json +++ b/plugins/PRTG/v1/dataStreams/probes.json @@ -82,7 +82,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -100,7 +100,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "condition", diff --git a/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js index c7905748..63bf7c0e 100644 --- a/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js +++ b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js @@ -3,53 +3,109 @@ // yields "Free Space C:", a ping sensor "Response Time"). A declared-column data // stream cannot express that, so unpivot to one row per channel per interval. // -// Timestamps need care. Unlike table.json's `*_raw` columns — which are OLE -// dates in UTC — historicdata.json only ever returns `datetime` as a string in -// the PRTG server's *local* time, with no raw/epoch equivalent. Emitting that -// as-is would put this stream an hour (or more) away from `Last Check` and the -// Log stream on the same dashboard, so convert local -> UTC here using the -// configured zone, which is interpolated into this script at request time. +// Timestamps have to come from the `datetime` string, awkward as that is. +// historicdata.json does expose a `datetime_raw` OLE date, but only when +// `usecaption` is omitted entirely — and in that mode every channel collapses +// into one unnamed `value` column, which defeats the whole point of this stream. +// The two are mutually exclusive. `datetime_raw` is also the bucket *end* while +// `datetime` shows the bucket range, so swapping to it would shift every point +// by one interval. Verified against PRTG 26.3.122.1665 (see the PR description). +// +// So: parse the wall clock, then convert it to UTC using the configured zone. +// Without that, this stream sits an hour (or more) away from `Last Check` and +// the Log stream on the same dashboard. +// +// The zone is interpolated at request time. IANA names only ever contain +// [A-Za-z0-9_+/-], so stripping everything else keeps a stray quote from +// terminating the string literal below and injecting into this script. const TIME_ZONE = - '{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; return tz || "UTC"; })() }}'; + '{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; return String(tz || "UTC").replace(/[^A-Za-z0-9_+\/-]/g, "") || "UTC"; })() }}'; + +// PRTG formats `datetime` in the account's regional setting, so the shape varies +// by installation. Parse the known forms explicitly rather than leaning on +// `new Date(str)`, which rejects the European form outright and would otherwise +// resolve the others against the *host* time zone instead of PRTG's. +// Returns a wall-clock instant expressed as if UTC, or null if unrecognised. +const parseNaive = (text) => { + // M/D/YYYY h:mm:ss AM|PM + let m = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2}):(\d{2})(?:\s*([AaPp])\.?[Mm]\.?)?$/); + if (m) { + let hour = Number(m[4]); + if (m[7]) { + if (hour === 12) hour = 0; + if (m[7].toUpperCase() === 'P') hour += 12; + } + return Date.UTC(Number(m[3]), Number(m[1]) - 1, Number(m[2]), hour, Number(m[5]), Number(m[6])); + } + // D.M.YYYY HH:mm:ss + m = text.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})\s+(\d{1,2}):(\d{2}):(\d{2})$/); + if (m) return Date.UTC(Number(m[3]), Number(m[2]) - 1, Number(m[1]), Number(m[4]), Number(m[5]), Number(m[6])); + // YYYY-MM-DD HH:mm:ss + m = text.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{1,2}):(\d{2}):(\d{2})/); + if (m) return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6])); + return null; +}; -// Offset (ms) that `zone` was running at the given instant. Reading an instant -// as a wall clock and re-parsing it as UTC yields exactly that offset. +// Offset (ms) that `zone` was running at the given instant. Read via +// formatToParts rather than a formatted string, so nothing depends on how a +// given ICU build separates the date from the time. Returns null if the parts +// do not come back as numbers, so no caller can reach `new Date(NaN)`. const offsetAt = (instant, zone) => { - const wall = new Date(instant).toLocaleString('sv-SE', { timeZone: zone }); - return Date.parse(wall.replace(' ', 'T') + 'Z') - instant; + const parts = new Intl.DateTimeFormat('en-GB', { + timeZone: zone, + hourCycle: 'h23', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).formatToParts(new Date(instant)); + + const p = {}; + for (const part of parts) p[part.type] = part.value; + + const wall = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(p.hour), Number(p.minute), Number(p.second)); + return isFinite(wall) ? wall - instant : null; }; -// Interpret a naive wall-clock string as a time in `zone` and return real UTC. -// Applied twice so a reading that lands near a DST transition resolves against -// the offset actually in force rather than the one on the other side of it. +// Interpret a naive wall-clock instant as a time in `zone` and return real UTC. +// Applied twice so a reading near a DST transition resolves against the offset +// actually in force rather than the one on the other side of it. const wallClockToUtc = (naive, zone) => { - let guess = naive - offsetAt(naive, zone); - guess = naive - offsetAt(guess, zone); - return guess; + const first = offsetAt(naive, zone); + if (first === null) return null; + const second = offsetAt(naive - first, zone); + return second === null ? null : naive - second; }; // `datetime` is either a single stamp ("8/20/2026 9:04:15 PM", when avg=0) or a -// bucket range ("8/20/2026 9:00:00 PM - 9:05:00 PM"). Take the bucket start. +// bucket range ("8/24/2026 11:00:00 AM - 11:05:00 AM"). Take the bucket start. const parseWhen = (raw) => { const text = String(raw || ''); const start = text.includes(' - ') ? text.split(' - ')[0].trim() : text.trim(); - const naive = new Date(start).getTime(); - if (isNaN(naive)) return null; + + const naive = parseNaive(start); + if (naive === null || !isFinite(naive)) return null; let utc = naive; try { - utc = wallClockToUtc(naive, TIME_ZONE); + const converted = wallClockToUtc(naive, TIME_ZONE); + // Unrecognised zone or unusable parts — fall back to treating the wall + // clock as UTC, matching the request side's own fallback. + if (converted !== null) utc = converted; } catch (e) { - // Unrecognised zone — fall back to treating the wall clock as UTC, - // matching the request side's own fallback. utc = naive; } - return new Date(utc).toISOString(); + + return isFinite(utc) ? new Date(utc).toISOString() : null; }; -// "100 %" -> 100 +// "100 %" -> 100. Test for null/undefined rather than falsiness so a genuine +// zero reports as 0% rather than as no reading at all. const parseCoverage = (raw) => { - const num = parseFloat(String(raw || '').replace('%', '').trim()); + if (raw === null || raw === undefined) return null; + const num = parseFloat(String(raw).replace('%', '').trim()); return isNaN(num) ? null : num; }; @@ -66,6 +122,9 @@ for (const row of rows) { for (const [channel, raw] of Object.entries(row)) { if (channel === 'datetime' || channel === 'coverage') continue; + // Only present when `usecaption` is omitted, which this stream never + // does — cheap insurance against them surfacing as channels. + if (channel.endsWith('_raw')) continue; // Intervals with no coverage come back as empty strings — drop them so // they leave a genuine gap in the chart rather than plotting as zero. diff --git a/plugins/PRTG/v1/dataStreams/sensorChannels.json b/plugins/PRTG/v1/dataStreams/sensorChannels.json index 59a03b4b..89504968 100644 --- a/plugins/PRTG/v1/dataStreams/sensorChannels.json +++ b/plugins/PRTG/v1/dataStreams/sensorChannels.json @@ -19,7 +19,7 @@ "pathToData": "channels", "paging": { "mode": "none" } }, - "matches": { "sourceType": { "type": "oneOf", "values": ["PRTG Sensor"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Sensor"] } }, "metadata": [ { "name": "objid", diff --git a/plugins/PRTG/v1/dataStreams/sensorHistory.json b/plugins/PRTG/v1/dataStreams/sensorHistory.json index 365e78d0..e160fcd9 100644 --- a/plugins/PRTG/v1/dataStreams/sensorHistory.json +++ b/plugins/PRTG/v1/dataStreams/sensorHistory.json @@ -21,11 +21,11 @@ }, { "key": "sdate", - "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.start); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var MAX = 30 * 86400000; var start = new Date(timeframe.start).getTime(); var end = new Date(timeframe.end).getTime(); if (end - start > MAX) { start = end - MAX; } var fmt = function(zone){ var parts = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, hourCycle: \"h23\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }).formatToParts(new Date(start)); var p = {}; for (var i = 0; i < parts.length; i++) { p[parts[i].type] = parts[i].value; } return p.year + \"-\" + p.month + \"-\" + p.day + \"-\" + p.hour + \"-\" + p.minute + \"-\" + p.second; }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" }, { "key": "edate", - "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var d = new Date(timeframe.end); var fmt = function(zone){ return d.toLocaleString(\"sv-SE\", { timeZone: zone }).replace(\" \", \"-\").replace(/:/g, \"-\"); }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" + "value": "{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; if (!tz) { tz = \"UTC\"; } var MAX = 30 * 86400000; var start = new Date(timeframe.start).getTime(); var end = new Date(timeframe.end).getTime(); var fmt = function(zone){ var parts = new Intl.DateTimeFormat(\"en-GB\", { timeZone: zone, hourCycle: \"h23\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" }).formatToParts(new Date(end)); var p = {}; for (var i = 0; i < parts.length; i++) { p[parts[i].type] = parts[i].value; } return p.year + \"-\" + p.month + \"-\" + p.day + \"-\" + p.hour + \"-\" + p.minute + \"-\" + p.second; }; try { return fmt(tz); } catch (e) { return fmt(\"UTC\"); } })() }}" }, { "key": "avg", @@ -38,7 +38,7 @@ "sourceType": { "type": "oneOf", "values": [ - "PRTG Sensor" + "Sensor" ] } }, diff --git a/plugins/PRTG/v1/dataStreams/sensors.json b/plugins/PRTG/v1/dataStreams/sensors.json index 0fe64f1b..49d91c86 100644 --- a/plugins/PRTG/v1/dataStreams/sensors.json +++ b/plugins/PRTG/v1/dataStreams/sensors.json @@ -104,7 +104,7 @@ "Paused by User", "Paused by Dependency", "Paused by Schedule", - "Paused by License", + "Not Licensed", "Paused until" ] } @@ -122,7 +122,7 @@ "displayName": "Status Text", "computed": true, "shape": "string", - "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Paused',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" + "valueExpression": "{{ ({0:'None',1:'Unknown',2:'Scanning',3:'Up',4:'Warning',5:'Down',6:'No Probe',7:'Paused',8:'Paused',9:'Paused',10:'Unusual',11:'Not Licensed',12:'Paused',13:'Down (Acknowledged)',14:'Down (Partial)'})[Number($['status_raw'])] || 'Unknown' }}" }, { "name": "message_raw", @@ -211,7 +211,7 @@ }, { "sourceId": "parentid", - "sourceType": "PRTG Device", + "sourceType": "Device", "name": "device" } ], diff --git a/plugins/PRTG/v1/scopes.json b/plugins/PRTG/v1/defaultContent/scopes.json similarity index 76% rename from plugins/PRTG/v1/scopes.json rename to plugins/PRTG/v1/defaultContent/scopes.json index a3ad5767..148e3dc8 100644 --- a/plugins/PRTG/v1/scopes.json +++ b/plugins/PRTG/v1/defaultContent/scopes.json @@ -2,7 +2,7 @@ { "name": "Probes", "matches": { - "sourceType": { "type": "oneOf", "values": ["PRTG Probe"] } + "sourceType": { "type": "oneOf", "values": ["Probe"] } }, "variable": { "name": "Probe", @@ -14,7 +14,7 @@ { "name": "Groups", "matches": { - "sourceType": { "type": "oneOf", "values": ["PRTG Group"] } + "sourceType": { "type": "oneOf", "values": ["Group"] } }, "variable": { "name": "Group", @@ -26,7 +26,7 @@ { "name": "Devices", "matches": { - "sourceType": { "type": "oneOf", "values": ["PRTG Device"] } + "sourceType": { "type": "oneOf", "values": ["Device"] } }, "variable": { "name": "Device", @@ -38,7 +38,7 @@ { "name": "Sensors", "matches": { - "sourceType": { "type": "oneOf", "values": ["PRTG Sensor"] } + "sourceType": { "type": "oneOf", "values": ["Sensor"] } }, "variable": { "name": "Sensor", diff --git a/plugins/PRTG/v1/docs/README.md b/plugins/PRTG/v1/docs/README.md index b0b6e9ab..3a0f21bd 100644 --- a/plugins/PRTG/v1/docs/README.md +++ b/plugins/PRTG/v1/docs/README.md @@ -28,7 +28,7 @@ You will need the **URL** of your PRTG server and a PRTG **API key**. | Field | What it is | Where to find it | Required | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- | | **PRTG URL** | The base address of your PRTG web interface — for example `https://prtg.example.com` or `https://yourname.my-prtg.com`. Include the sub-path if PRTG sits behind a reverse proxy, but no query string. | The address bar of your PRTG web interface. | Yes | -| **API key** | Authenticates every request. Sent as the `apitoken` query parameter, which is the only scheme PRTG's v1 API accepts. | PRTG → **Setup → Account Settings → API Keys**. | Yes | +| **API key** | Authenticates every request. Sent as the `apitoken` query parameter — PRTG rejects the key in an `Authorization` header. | PRTG → **Setup → Account Settings → API Keys**. | Yes | | **PRTG time zone** | The time zone of the PRTG account whose API key you supplied, as an IANA name such as `Europe/London`. Only affects **Sensor History** and **Log**. Defaults to UTC. | PRTG → **Setup → Account Settings → My Account → Time Zone**. | No | | **Ignore certificate errors** | Skips TLS certificate validation. Only enable for an on-premise PRTG server using a self-signed certificate. | — | No | @@ -75,14 +75,14 @@ for each **Probe**, **Group**, **Device** and **Sensor**. | Object type | API source | Represents | | ---------------- | ------------------------------------------------------ | -------------------------------------------------------------- | -| **PRTG Probe** | `GET /api/table.json?content=probes&filter_type=probenode` | A local or remote probe that performs monitoring. | -| **PRTG Group** | `GET /api/table.json?content=groups` | A group of devices. Groups can nest inside other groups. | -| **PRTG Device** | `GET /api/table.json?content=devices` | A monitored host, identified by its address. | -| **PRTG Sensor** | `GET /api/table.json?content=sensors` | A single check running against a device. | +| **Probe** | `GET /api/table.json?content=probes&filter_type=probenode` | A local or remote probe that performs monitoring. | +| **Group** | `GET /api/table.json?content=groups` | A group of devices. Groups can nest inside other groups. | +| **Device** | `GET /api/table.json?content=devices` | A monitored host, identified by its address. | +| **Sensor** | `GET /api/table.json?content=sensors` | A single check running against a device. | **Relationships:** every object stores its PRTG parent's id as a `parentId` property, and sensors also store `deviceId`, `deviceName`, `groupName` and `probeName`. The **Sensors** stream links its Device column -straight to the **PRTG Device** object, so you can click through from a sensor to the device it runs on. See +straight to the **Device** object, so you can click through from a sensor to the device it runs on. See the first limitation below for why these are properties rather than graph relationships. **Sites:** PRTG has no "site" object. Probes, groups and devices each store a `location` property holding @@ -107,7 +107,8 @@ are the usual way to organise by site. The **Sites** dashboard groups devices by - **Sensor History is limited to 30 days**, and beyond a week it is averaged hourly. Finer buckets over a long range return more rows than the platform's response size limit allows, so when you have not chosen an **Averaging interval** the plugin asks PRTG for hourly figures on ranges longer than seven days. Choosing - **5 minutes** or **Raw** explicitly on a long range can still exceed the limit and fail the tile. + **5 minutes** or **Raw** explicitly on a long range can still exceed the limit and fail the tile. A tile + following a dashboard timeframe longer than 30 days shows the most recent 30 days rather than failing. - **The log returns at most 5,000 entries per query.** PRTG returns newest first, so on a busy installation over a long timeframe the oldest entries in the range are dropped without warning. Unlike the object tables, PRTG reports no usable total for the log, so there is no way to detect that truncation happened — @@ -127,7 +128,9 @@ are the usual way to organise by site. The **Sites** dashboard groups devices by - **Historic data granularity is PRTG's.** PRTG aggregates history into buckets and returns a `coverage` percentage per bucket; intervals PRTG has no data for are omitted rather than plotted as zero. Choosing **Raw** on **Sensor History** is only practical over short timeframes. -- **The API key travels in the query string.** PRTG's v1 API rejects `Authorization: Bearer`, so the token - must be sent as the `apitoken` query parameter. Always use HTTPS. +- **The API key travels in the query string.** PRTG's manual documents `Authorization: Bearer` for API keys, + but PRTG rejects it in practice — `Bearer`, `X-Api-Key` and `Authorization: apitoken` all answer + `401 Unsupported authorization scheme` on 26.3.122.1665, leaving the `apitoken` query parameter as the only + scheme that works. Always use HTTPS. - **Read-only.** The plugin never creates, modifies, acknowledges, pauses or deletes anything in PRTG, and a **Read access** API key is all it needs. diff --git a/plugins/PRTG/v1/indexDefinitions/default.json b/plugins/PRTG/v1/indexDefinitions/default.json index a4c60383..8f2fe4f7 100644 --- a/plugins/PRTG/v1/indexDefinitions/default.json +++ b/plugins/PRTG/v1/indexDefinitions/default.json @@ -10,7 +10,7 @@ "id": "objid", "name": "name", "type": { - "value": "PRTG Probe" + "value": "Probe" }, "properties": [ { @@ -32,7 +32,7 @@ "id": "objid", "name": "name", "type": { - "value": "PRTG Group" + "value": "Group" }, "properties": [ { @@ -60,7 +60,7 @@ "id": "objid", "name": "name", "type": { - "value": "PRTG Device" + "value": "Device" }, "properties": [ { @@ -94,7 +94,7 @@ "id": "objid", "name": "name", "type": { - "value": "PRTG Sensor" + "value": "Sensor" }, "properties": [ { diff --git a/plugins/PRTG/v1/metadata.json b/plugins/PRTG/v1/metadata.json index fdde78b7..27377243 100644 --- a/plugins/PRTG/v1/metadata.json +++ b/plugins/PRTG/v1/metadata.json @@ -19,10 +19,10 @@ "infrastructure" ], "objectTypes": [ - "PRTG Probe", - "PRTG Group", - "PRTG Device", - "PRTG Sensor" + "Probe", + "Group", + "Device", + "Sensor" ], "links": [ { From fc4fa534cc42e88950b0e69b093073fad600a8a9 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 24 Aug 2026 12:44:17 +0100 Subject: [PATCH 3/7] Condense the sensorHistory.js header comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the narrative framing and subjective wording, keeping the reason `datetime_raw` is unusable — it is only returned when `usecaption` is omitted, and it carries the bucket end rather than its start. Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/dataStreams/scripts/sensorHistory.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js index 63bf7c0e..700d86ae 100644 --- a/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js +++ b/plugins/PRTG/v1/dataStreams/scripts/sensorHistory.js @@ -3,21 +3,16 @@ // yields "Free Space C:", a ping sensor "Response Time"). A declared-column data // stream cannot express that, so unpivot to one row per channel per interval. // -// Timestamps have to come from the `datetime` string, awkward as that is. -// historicdata.json does expose a `datetime_raw` OLE date, but only when -// `usecaption` is omitted entirely — and in that mode every channel collapses -// into one unnamed `value` column, which defeats the whole point of this stream. -// The two are mutually exclusive. `datetime_raw` is also the bucket *end* while -// `datetime` shows the bucket range, so swapping to it would shift every point -// by one interval. Verified against PRTG 26.3.122.1665 (see the PR description). +// Timestamps come from the `datetime` string. historicdata.json also returns a +// `datetime_raw` OLE date, but only when `usecaption` is omitted — a mode that +// collapses every channel into one unnamed `value` column — and it carries the +// bucket end rather than its start, so neither form is usable here (checked +// against PRTG 26.3.122.1665). // -// So: parse the wall clock, then convert it to UTC using the configured zone. -// Without that, this stream sits an hour (or more) away from `Last Check` and -// the Log stream on the same dashboard. -// -// The zone is interpolated at request time. IANA names only ever contain -// [A-Za-z0-9_+/-], so stripping everything else keeps a stray quote from -// terminating the string literal below and injecting into this script. +// `datetime` is a local wall clock, so convert it to UTC with the configured +// zone, or this stream sits an hour or more from `Last Check` and the Log stream +// on the same dashboard. IANA names contain only [A-Za-z0-9_+/-]; stripping +// other characters stops a quote in a custom value terminating the literal below. const TIME_ZONE = '{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; return String(tz || "UTC").replace(/[^A-Za-z0-9_+\/-]/g, "") || "UTC"; })() }}'; From 543a168b3a501f7a286c68276470ad264e3737f2 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 27 Aug 2026 10:14:59 +0100 Subject: [PATCH 4/7] Warn on setup when the PRTG time zone is wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRTG's v1 API interprets `filter_dstart`/`filter_dend` in the time zone of the account whose API key is in use and offers no UTC option, so the zone has to be configured. Until now a wrong answer failed silently: Log and Sensor History shifted by the difference, or looked empty on a short timeframe, with nothing to point at the cause. `getstatus.htm` reports the account's zone as a fixed offset in `UserTimeZone`, which System Status already surfaces. Too coarse to replace the setting — Sensor History spans 30 days and needs the daylight saving transitions an IANA name carries — but enough to check it against. Adds a hidden `timeZoneCheck` stream and a non-blocking Time zone step in configValidation. Two things worth knowing about the comparison: - It accepts the zone's current offset *or* its standard one. PRTG labels zones by standard offset in its own UI, so which of the two `UserTimeZone` carries is unknown. Accepting both still catches a zone picked on the wrong continent without crying wolf every summer. - A `UserTimeZone` it cannot parse passes rather than warns — a PRTG version that words the field differently must not fail setup. A bad IANA name is compared as UTC, because that is what the request side falls back to sending. The step returns one row on agreement and none on mismatch because `errorOnEmptyResults` can only fail a step on empty. It is `required: false` so a false positive cannot block anyone from connecting. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/PRTG/v1/configValidation.json | 10 +++ .../v1/dataStreams/scripts/timeZoneCheck.js | 88 +++++++++++++++++++ .../PRTG/v1/dataStreams/timeZoneCheck.json | 24 +++++ plugins/PRTG/v1/docs/README.md | 14 ++- plugins/PRTG/v1/ui.json | 2 +- 5 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 plugins/PRTG/v1/dataStreams/scripts/timeZoneCheck.js create mode 100644 plugins/PRTG/v1/dataStreams/timeZoneCheck.json diff --git a/plugins/PRTG/v1/configValidation.json b/plugins/PRTG/v1/configValidation.json index c8f38187..1bb49670 100644 --- a/plugins/PRTG/v1/configValidation.json +++ b/plugins/PRTG/v1/configValidation.json @@ -6,6 +6,16 @@ "required": true, "error": "Could not connect to PRTG. Check the PRTG URL is reachable from SquaredUp, and that the API key is correct and has not been deleted.", "success": "Connected to PRTG successfully." + }, + { + "displayName": "Time zone", + "dataStream": { + "name": "timeZoneCheck", + "config": { "errorOnEmptyResults": true } + }, + "required": false, + "error": "The PRTG time zone selected does not match the one PRTG reports. Compare it against Setup → Account Settings → My Account → Time Zone in PRTG — until they agree, Log and Sensor History will be shifted by the difference.", + "success": "Time zone matches PRTG." } ] } diff --git a/plugins/PRTG/v1/dataStreams/scripts/timeZoneCheck.js b/plugins/PRTG/v1/dataStreams/scripts/timeZoneCheck.js new file mode 100644 index 00000000..b959c99f --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/scripts/timeZoneCheck.js @@ -0,0 +1,88 @@ +// PRTG's legacy API interprets `filter_dstart`/`filter_dend` and reports historic +// data in the time zone of the *account* whose API key is in use, and offers no +// way to ask for UTC. That zone therefore has to be configured, and getting it +// wrong shifts Log and Sensor History by hours with nothing to show for it. +// +// It can, however, be read back: `getstatus.htm` reports the account's zone as a +// fixed offset in `UserTimeZone` (for example "UTC-03:00"). That is too coarse to +// replace the setting — Sensor History spans up to 30 days and needs the daylight +// saving transitions an IANA name carries — but it is enough to check it. This +// stream exists only for the `Time zone` step in configValidation.json, which +// treats no rows as a mismatch, so agreement has to be the non-empty case. +// +// IANA names contain only [A-Za-z0-9_+/-]; stripping other characters stops a +// quote in a custom value terminating the literal below. +const TIME_ZONE = + '{{ (function(){ var v = dataSource.serverTimeZone; var tz = Array.isArray(v) ? (v[0] && v[0].value) : v; return String(tz || "UTC").replace(/[^A-Za-z0-9_+\/-]/g, "") || "UTC"; })() }}'; + +// Offset (minutes) that `zone` was running at the given instant. Read via +// formatToParts rather than a formatted string, so nothing depends on how a given +// ICU build separates the date from the time. Returns null if the parts do not +// come back as numbers, so no caller can reach `new Date(NaN)`. +const offsetAt = (instant, zone) => { + const parts = new Intl.DateTimeFormat('en-GB', { + timeZone: zone, + hourCycle: 'h23', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).formatToParts(new Date(instant)); + + const p = {}; + for (const part of parts) p[part.type] = part.value; + + const wall = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(p.hour), Number(p.minute), Number(p.second)); + return isFinite(wall) ? Math.round((wall - instant) / 60000) : null; +}; + +// PRTG labels a zone by its *standard* offset in its own UI, so it is not safe to +// assume `UserTimeZone` reports the offset in force right now. Accept either the +// current offset or the standard one — daylight saving only ever adds to the +// offset, so the standard offset is the smaller of the two solstice readings in +// both hemispheres. Accepting both still catches the mistakes that matter (a zone +// picked on the wrong continent) without crying wolf every summer. +// +// An unusable zone falls back to UTC, matching what the request side in logs.json +// and sensorHistory.json does with one — the queries really are being sent as UTC +// in that case, so a non-UTC server should still be flagged. +const candidateOffsets = (zone) => { + try { + const now = Date.now(); + const year = new Date(now).getUTCFullYear(); + const winter = offsetAt(Date.UTC(year, 0, 1), zone); + const summer = offsetAt(Date.UTC(year, 6, 1), zone); + const current = offsetAt(now, zone); + if (winter === null || summer === null || current === null) return [0]; + return [current, Math.min(winter, summer)]; + } catch (e) { + return [0]; + } +}; + +// "UTC-03:00" -> -180. Also accepts a bare "UTC", tolerates the separator being +// dropped ("UTC+0530"), and reads the offset out of the bracketed form PRTG +// labels zones with in its own UI ("(UTC+01:00) Amsterdam, Berlin") in case this +// field ever carries that instead. Returns null for anything else, which the +// caller reads as "cannot tell" rather than "mismatch" — a PRTG version that +// words this field differently must not fail the check. +const parseReportedOffset = (raw) => { + const m = String(raw == null ? '' : raw) + .trim() + .match(/^\(?\s*(?:UTC|GMT)(?:\s*([+-])\s*(\d{1,2}):?(\d{2})?)?\s*(?:\)|$)/i); + if (!m) return null; + if (!m[1]) return 0; + const minutes = Number(m[2]) * 60 + Number(m[3] || 0); + return m[1] === '-' ? -minutes : minutes; +}; + +const reportedRaw = data && typeof data === 'object' ? data.UserTimeZone : null; +const reported = parseReportedOffset(reportedRaw); + +// No row means "mismatch" to configValidation, so every uncertain case has to +// return one: a value it cannot read, or a status body PRTG did not answer with. +const agrees = reported === null || candidateOffsets(TIME_ZONE).includes(reported); + +result = agrees ? [{ serverTimeZone: reportedRaw === null || reportedRaw === undefined ? TIME_ZONE : String(reportedRaw) }] : []; diff --git a/plugins/PRTG/v1/dataStreams/timeZoneCheck.json b/plugins/PRTG/v1/dataStreams/timeZoneCheck.json new file mode 100644 index 00000000..2c1012ea --- /dev/null +++ b/plugins/PRTG/v1/dataStreams/timeZoneCheck.json @@ -0,0 +1,24 @@ +{ + "name": "timeZoneCheck", + "displayName": "Time Zone Check", + "description": "Compares the configured PRTG time zone against the zone the PRTG server reports it is using", + "tags": ["Status"], + "baseDataSourceName": "httpRequestUnscoped", + "visibility": { "type": "hidden" }, + "config": { + "httpMethod": "get", + "endpointPath": "getstatus.htm", + "getArgs": [{ "key": "id", "value": "0" }], + "postRequestScript": "timeZoneCheck.js" + }, + "matches": "none", + "metadata": [ + { + "name": "serverTimeZone", + "displayName": "Server Time Zone", + "shape": "string", + "role": "label" + } + ], + "timeframes": false +} diff --git a/plugins/PRTG/v1/docs/README.md b/plugins/PRTG/v1/docs/README.md index 3a0f21bd..603c7b93 100644 --- a/plugins/PRTG/v1/docs/README.md +++ b/plugins/PRTG/v1/docs/README.md @@ -21,7 +21,8 @@ You will need the **URL** of your PRTG server and a PRTG **API key**. delete the key and create a new one. 7. Paste the key into the **API key** field, and your PRTG address into **PRTG URL**. 8. Set **PRTG time zone** to the time zone of the account whose key you just created — it is shown in PRTG - under **Setup → Account Settings → My Account → Time Zone**. + under **Setup → Account Settings → My Account → Time Zone**. Saving checks your answer against the zone + PRTG reports, so a mismatch is flagged there and then. ## Configuration fields @@ -32,8 +33,10 @@ You will need the **URL** of your PRTG server and a PRTG **API key**. | **PRTG time zone** | The time zone of the PRTG account whose API key you supplied, as an IANA name such as `Europe/London`. Only affects **Sensor History** and **Log**. Defaults to UTC. | PRTG → **Setup → Account Settings → My Account → Time Zone**. | No | | **Ignore certificate errors** | Skips TLS certificate validation. Only enable for an on-premise PRTG server using a self-signed certificate. | — | No | -On save, the plugin calls PRTG's status endpoint to confirm the URL and key. A failure means the URL is -unreachable or the key is invalid, expired, or deleted. +On save, the plugin calls PRTG's status endpoint twice: once to confirm the URL and key — a failure there +means the URL is unreachable or the key is invalid, expired, or deleted — and once to compare **PRTG time +zone** against the zone PRTG says it is using. A zone mismatch is a warning rather than a failure, so it +will not stop you connecting. > ⚠️ **Get the time zone right.** PRTG interprets date ranges in its *own* time zone rather than UTC, so the > wrong zone shifts **Sensor History** and **Log** — and on short timeframes can make them look empty. @@ -100,6 +103,11 @@ are the usual way to organise by site. The **Sites** dashboard groups devices by **Sensor History** timestamps to UTC, because that endpoint reports times only as local text with no UTC equivalent — so the wrong zone shifts both the range queried *and* the times plotted. An unrecognised zone name falls back to UTC rather than failing the request. + + PRTG does report the zone it is using, as a fixed offset — the **System Status** data stream surfaces it as + **Server Time Zone**, and saving the configuration warns when it disagrees with the zone you picked. That + offset cannot replace the setting, though: it describes only the present moment, whereas **Sensor History** + covers up to 30 days and needs the daylight saving transitions an IANA name carries. - **Very large installations may hit a response size limit.** Each object type is fetched in a single request rather than page by page. In practice the sensor import is the binding constraint and should comfortably handle around 10,000 sensors, which is also Paessler's own recommended maximum per core diff --git a/plugins/PRTG/v1/ui.json b/plugins/PRTG/v1/ui.json index c0b32649..013254da 100644 --- a/plugins/PRTG/v1/ui.json +++ b/plugins/PRTG/v1/ui.json @@ -27,7 +27,7 @@ "type": "autocomplete", "allowCustomValues": true, "isClearable": true, - "help": "The time zone of the PRTG **account** whose API key you entered above — PRTG interprets date ranges in that zone rather than UTC. Find it in PRTG under **Setup → Account Settings → My Account → Time Zone**. Daylight saving is handled automatically, so pick the region (for example `Europe/London`) rather than a fixed offset. Any [IANA time zone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) can be typed in. Defaults to UTC if left empty — the **System Status** data stream reports the zone PRTG is actually using.", + "help": "The time zone of the PRTG **account** whose API key you entered above — PRTG interprets date ranges in that zone rather than UTC. Find it in PRTG under **Setup → Account Settings → My Account → Time Zone**. Daylight saving is handled automatically, so pick the region (for example `Europe/London`) rather than a fixed offset. Any [IANA time zone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) can be typed in. Defaults to UTC if left empty. Saving checks this against the zone PRTG reports and warns if they disagree; the **System Status** data stream also shows it as **Server Time Zone**.", "data": { "source": "fixed", "values": [ From dd06067711fe072e8613d616dd5dbf8e7c5618b9 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 27 Aug 2026 12:13:22 +0100 Subject: [PATCH 5/7] Explain why the PRTG plugin uses API v1 Answers the review question on whether this should target PRTG API v2. Checked against the published API v2 OpenAPI specification: v2 has no log endpoint, no arbitrary historic-data range, and no time zone endpoint, so the Log and Sensor History streams and the setup time zone check could not be built on it. What remains is marked experimental, and v2 is unavailable on Hosted Monitor and off by default on existing installations. Records the conditions under which a v2-based plugin becomes worth having, as a new major version rather than a change to this one. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/PRTG/v1/docs/README.md | 72 ++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/plugins/PRTG/v1/docs/README.md b/plugins/PRTG/v1/docs/README.md index 603c7b93..f90773cb 100644 --- a/plugins/PRTG/v1/docs/README.md +++ b/plugins/PRTG/v1/docs/README.md @@ -2,9 +2,10 @@ Monitor your [PRTG Network Monitor](https://www.paessler.com/prtg) installation devices and sensors, with current status, channel readings, historic sensor data and the PRTG log — via the [PRTG HTTP API](https://www.paessler.com/manuals/prtg/http_api). -> ⚠️ This plugin uses the **PRTG API v1** (`/api/table.json`). It does not use PRTG API v2, whose object -> endpoints are still marked experimental by Paessler. Any PRTG version that supports API keys will work, -> including PRTG Hosted Monitor, PRTG Network Monitor and PRTG Enterprise Monitor. +> ⚠️ **This plugin uses the PRTG API v1** — `/api/table.json`, `/api/historicdata.json` and +> `/api/getstatus.htm`. Any PRTG version that supports API keys will work, including PRTG Network Monitor, +> PRTG Enterprise Monitor and PRTG Hosted Monitor, and you do **not** need to enable the new UI or API v2. +> See [Why this plugin uses API v1](#why-this-plugin-uses-api-v1) for the reasoning. ## Setup @@ -142,3 +143,68 @@ are the usual way to organise by site. The **Sites** dashboard groups devices by scheme that works. Always use HTTPS. - **Read-only.** The plugin never creates, modifies, acknowledges, pauses or deletes anything in PRTG, and a **Read access** API key is all it needs. + +## Why this plugin uses API v1 + +PRTG also has a newer [API v2](https://www.paessler.com/support/prtg/api/v2/overview/index.html), and where +it is stable it is the better API: ISO 8601 timestamps rather than Excel-style serial numbers, typed status +enumerations rather than numeric codes, native latitude and longitude, a sensor status summary embedded in +every probe, group and device, and a `path` array giving each object its place in the tree. It cannot yet +run this plugin, for three separate reasons. + +### 1. Endpoints this plugin needs that API v2 does not have + +Checked against the published +[API v2 OpenAPI specification](https://www.paessler.com/support/prtg/api/v2/oas/prtg.api.yaml): + +| What the plugin needs | API v1 | API v2 | +| --------------------- | ------ | ------ | +| List every probe, group, device and sensor, for indexing | `table.json?content=…&count=50000` — one request per type | Only `GET /experimental/{probes,groups,devices,sensors}`. The non-experimental equivalents are deprecated, and the stable endpoints are single-object `GET /{type}/{id}` lookups. Capped at 3,000 objects per request. | +| Everything beneath one probe, group or device | `table.json?content=sensors&id=…` — PRTG walks the subtree | Only through the experimental `filter` parameter on those experimental list endpoints. | +| Channel readings for one sensor | `table.json?content=channels&id=…` | `GET /sensors/{id}/data` — **stable, and better than API v1.** | +| Historic readings over an arbitrary window | `historicdata.json?sdate=&edate=&avg=` — any range, and a choice of raw, 5-minute, hourly or daily buckets | `GET /experimental/timeseries/{id}/{type}`, where `type` is one of four fixed windows: `live` (4 hours), `short` (2 days), `medium` (60 days), `long` (365 days). No arbitrary range and no averaging control. | +| PRTG log entries over a timeframe | `table.json?content=messages&filter_dstart=&filter_dend=` | **Nothing.** The specification contains no log, message or event endpoint. | +| Installation-wide sensor counts, version and edition | `getstatus.htm?id=0` — one request | `GET /sensor-status-summary` and `GET /version` are stable and cover most of it, and the experimental `/license` covers the edition — but that is three requests instead of one, and the new-message count, new-alarm count and server clock have no equivalent. | +| The PRTG account's time zone | `getstatus.htm?id=0` → `UserTimeZone` | **Nothing.** The word "timezone" does not appear in the specification. | + +On API v2, then, the **Log** data stream could not be built at all, **Sensor History** could not follow a +dashboard timeframe, and the time zone check performed when you save the configuration would not be +possible. + +### 2. What remains is marked experimental + +Paessler define an experimental endpoint as one that "might change between releases", and API v2 has +already moved: the plain `/probes`, `/groups`, `/devices`, `/sensors` and `/channels` endpoints are +deprecated in favour of `/experimental/…` ones, the original `/experimental/timeseries/{id}` is deprecated +in favour of `/experimental/timeseries/{id}/{type}`, `/experimental/channels` is simultaneously deprecated +*and* experimental, and the API was substantially reworked in PRTG 24.3.100. Every object list this plugin +indexes would sit on an endpoint Paessler reserve the right to change. + +API v1 carries the lower churn risk today, not the higher one. It is not deprecated, it has no announced +end of life, and Paessler's own API v2 reference still says that if you cannot achieve your objective with +API v2 you can use API v1 instead. + +### 3. API v2 is not available everywhere API v1 is + +Per Paessler's [guidance on the new UI and API v2](https://helpdesk.paessler.com/en/support/solutions/articles/76000063881-i-want-to-use-the-new-ui-and-api-v2-what-do-i-need-to-know-), +API v2 is not available on PRTG Hosted Monitor at all, clusters are not supported, and on an existing +installation it stays off until an administrator enables it under **Setup → Activate New UI And API v2**, +which needs ports 1615, 1616 and 23580 free on the PRTG server. It is only on by default for installations +created since PRTG 25.2.106. + +A low-code plugin has a single base URL and a single authentication configuration, so using API v2 even for +part of the data would make all of that a prerequisite for using the plugin at all, and would end Hosted +Monitor support. The one stream that would benefit — Sensor Channels — is not worth that trade. + +### When this should be revisited + +A separate PRTG plugin built on API v2 becomes worth having once: + +1. API v2 exposes a log or messages endpoint that can be filtered by date; +2. historic data can be requested for an arbitrary start and end time, with a choice of averaging interval; +3. the probe, group, device and sensor list endpoints leave `/experimental/` without being deprecated, and + offer either a subtree filter or a page size that makes indexing tens of thousands of sensors practical; +4. API v2 reaches PRTG Hosted Monitor, or dropping Hosted Monitor support becomes an accepted trade. + +The first two are hard blockers; the rest are cost. Because moving off API v1 would break existing users, +it belongs in a new major version of this plugin alongside this one rather than as a change to it. From 33f28631b10f974ac77e99de885f45236fe21948 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 27 Aug 2026 12:13:34 +0100 Subject: [PATCH 6/7] Document the PRTG time zone constraint and the UTC workaround MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API v1 time zone is a property of the PRTG account the key belongs to, not of the request — Paessler document no per-request override. Cite that, and note API v2 has no time zone endpoint either. Because the zone follows the account, pointing the key at a dedicated PRTG account set to UTC removes the problem entirely, daylight saving included. That was not written down anywhere; add it to Setup, worded so it does not read as contradicting the advice to pick a region rather than an offset. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/PRTG/v1/docs/README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/PRTG/v1/docs/README.md b/plugins/PRTG/v1/docs/README.md index f90773cb..ea2560db 100644 --- a/plugins/PRTG/v1/docs/README.md +++ b/plugins/PRTG/v1/docs/README.md @@ -25,6 +25,13 @@ You will need the **URL** of your PRTG server and a PRTG **API key**. under **Setup → Account Settings → My Account → Time Zone**. Saving checks your answer against the zone PRTG reports, so a mismatch is flagged there and then. +> **Tip: give SquaredUp its own PRTG account, set to UTC.** PRTG has no per-request time zone parameter — +> every response uses the time zone of the account the key belongs to. Change that account's time zone to +> **UTC** in PRTG, create the key there, and answer `UTC` here: daylight saving stops mattering, and nobody +> editing their personal time zone can silently shift your dashboards. Note this means genuinely setting the +> PRTG account to UTC — answering `UTC` for an account still on another zone is the mismatch the save-time +> check exists to catch. + ## Configuration fields | Field | What it is | Where to find it | Required | @@ -99,11 +106,14 @@ are the usual way to organise by site. The **Sites** dashboard groups devices by code-based plugins, not low-code ones like this. The PRTG hierarchy is therefore expressed as properties (`parentId`, `deviceName`, `groupName`, `probeName`) and through dashboard scoping and drilldown, rather than as traversable parent/child links in the graph. -- **Date ranges follow PRTG's own time zone.** See the **PRTG time zone** field above. PRTG offers no way to - query in UTC and accepts no relative ranges, so the zone has to be supplied. It is also used to convert - **Sensor History** timestamps to UTC, because that endpoint reports times only as local text with no UTC - equivalent — so the wrong zone shifts both the range queried *and* the times plotted. An unrecognised zone - name falls back to UTC rather than failing the request. +- **Date ranges follow the API account's time zone.** See the **PRTG time zone** field above. The zone is a + property of the PRTG *account* the key belongs to, and Paessler + [document no per-request override](https://helpdesk.paessler.com/en/support/solutions/articles/76000073098-output-of-api-table-json-content-messages-contains-datetime-is-it-in-gmt-utc-filter-dstart) — there is no + UTC parameter and no relative ranges — so the zone has to be supplied. API v2 does not help; it exposes no + time zone endpoint at all. The zone is also used to convert **Sensor History** timestamps to UTC, because + that endpoint reports times only as local text with no UTC equivalent — so the wrong zone shifts both the + range queried *and* the times plotted. An unrecognised zone name falls back to UTC rather than failing the + request. PRTG does report the zone it is using, as a fixed offset — the **System Status** data stream surfaces it as **Server Time Zone**, and saving the configuration warns when it disagrees with the zone you picked. That From 200b7819d0d5cfd4f850965578935ecfdc8a43c0 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 27 Aug 2026 12:13:41 +0100 Subject: [PATCH 7/7] Use the PRTG gauge mark alone as the plugin icon The icon was the stacked "PRTG / NETWORK / MONITOR" lockup. Each text line was only about 63px tall inside the 512 canvas, so at tile size the words rendered around 4px tall and read as a smudge. Drop the three wordmark paths and rescale the four gauge-arc paths, which were already in the file, to fill the square. Keep the white plate: it is the convention for the other 512-viewBox icons here, and the needle is navy, so it would disappear on a dark tile without one. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/PRTG/v1/icon.svg | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/plugins/PRTG/v1/icon.svg b/plugins/PRTG/v1/icon.svg index 6cb211ed..fec7195a 100644 --- a/plugins/PRTG/v1/icon.svg +++ b/plugins/PRTG/v1/icon.svg @@ -1,32 +1,25 @@ - + - - + + + + + + + d="m 2124.33,1394 c 38.66,-48.27 62.52,-108.81 64.42,-175.11 1.52,-52.89 -11.15,-102.89 -34.47,-146.54 l 138.46,-84.854 c 64.95,91.814 93.1,208.864 68.37,327.364 -17.51,83.83 -59.23,156.5 -116.17,212.76" /> + +