Skip to content

jonth93/acsmon-nodejs-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ACS Monitor — Node.js API Client

Open-source Node.js client library for the ACS Monitor REST API — the network and infrastructure monitoring platform from Anglia Computer Solutions.

Zero-dependency Node.js client using the built-in fetch from Node 18+. ESM module, drop-in for any modern Node project (TypeScript or JavaScript).

Licensing: this Node.js client is open source under the MIT licence. The ACS Monitor application it talks to is licensed commercial software with a free 100-device tier — see acsmon.com for tiers and pricing.

Contents

Requirements

Requirement Minimum Notes
Node.js 18.0 Uses native fetch and AbortSignal.timeout
Module system ESM The client is shipped as .mjs. CommonJS users can import() it dynamically.
Network Outbound to ACSMON_BASE_URL Whatever host runs your ACS Monitor install

No npm dependencies. No build step. No transpiler.

Installation

There is nothing to publish to npm — this client is a single file.

Option A — copy the file into your project:

cp api-clients/nodejs/src/client.mjs path/to/your-app/lib/acsmon.mjs

Option B — git submodule (so you can git pull future updates):

git submodule add https://github.com/jonth93/acsmon-nodejs-client.git vendor/acsmon
import { AcsMonitorClient } from './vendor/acsmon/src/client.mjs';

Option C — run the examples directly from this folder:

cd api-clients/nodejs
node examples/list-devices.mjs

Quick start

cd api-clients/nodejs
export ACSMON_BASE_URL=https://monitoring.example.com
export [email protected]
export ACSMON_PASSWORD='your-strong-password'

node examples/list-devices.mjs
node examples/poll-device.mjs 42

If ACSMON_TOKEN is set, the example scripts skip the login step.

Configuration

import { AcsMonitorClient } from './src/client.mjs';

const api = new AcsMonitorClient(
    'https://monitoring.example.com',  // base URL (no /api/v1 suffix needed)
    process.env.ACSMON_TOKEN ?? null,  // optional pre-existing bearer token
    { timeoutMs: 30000 },              // optional per-request timeout
);
Constructor arg Type Default Purpose
baseUrl string required Root URL of your ACS Monitor install. The client appends /api/v1. Trailing slashes stripped.
token string | null null Long-lived bearer token. Skip if you intend to call login().
options.timeoutMs number 30000 Aborts each request after N ms via AbortSignal.timeout.

The client never persists state to disk — the bearer token only lives inside the AcsMonitorClient instance.

Authentication

The API uses bearer token authentication. There are two ways to get one:

1. Trade email + password for a token:

const api = new AcsMonitorClient('https://monitoring.example.com');
const token = await api.login('[email protected]', 'secret');
// `api` will now send Authorization: Bearer <token> on every call.
// `token` is also returned so you can persist it.

2. Reuse a token you've already obtained (preferred for daemons, cron jobs, serverless functions):

const api = new AcsMonitorClient('https://monitoring.example.com', process.env.ACSMON_TOKEN);
// no login() call needed — the token is sent immediately.

To revoke the token server-side:

await api.logout();   // POST /auth/logout, then clears the in-memory token

For long-running integrations, create a dedicated API user in the ACS Monitor UI (Settings → Users) with the minimum role needed (viewer for read-only, operator for ack/resolve, admin for full write access). Tokens live until logout or user deletion.

Method reference

Methods that return an array of rows are async iteratorsfor await (...) {} walks every page automatically. Single-resource methods return a Promise<object>.

Auth

Method HTTP Returns
login(email, password) POST /auth/login Promise<string> (token)
logout() POST /auth/logout Promise<void>

Devices (SNMP)

Method HTTP Returns
devices(query?) GET /devices async iterator
device(id) GET /devices/{id} Promise<Device>
pollDevice(id) POST /devices/{id}/poll Promise<{queued, job_id}>
deviceMetrics(id, query?) GET /devices/{id}/metrics Promise<Metrics>
deviceAlerts(id, query?) GET /devices/{id}/alerts Promise<Page<AlertEvent>>

Monitors (service checks)

Method HTTP Returns
monitors(query?) GET /monitors async iterator
monitor(id) GET /monitors/{id} Promise<Monitor>
createMonitor(payload) POST /monitors Promise<Monitor>
checkMonitor(id) POST /monitors/{id}/check Promise<{queued, job_id}>
monitorResults(id, query?) GET /monitors/{id}/results Promise<Page<Result>>
monitorUptime(id, query?) GET /monitors/{id}/uptime Promise<UptimeStats>

Alerts

Method HTTP Returns
alertEvents(query?) GET /alert-events async iterator
acknowledgeAlertEvent(id, note?) POST /alert-events/{id}/acknowledge Promise<AlertEvent>
resolveAlertEvent(id, note?) POST /alert-events/{id}/resolve Promise<AlertEvent>

System

Method HTTP Returns
systemHealth() GET /system/health Promise<HealthReport>

Returned data shapes

Every list response follows the same envelope:

{
  "data": [ /* rows */ ],
  "links": {
    "first": "https://.../api/v1/devices?page=1",
    "last":  "https://.../api/v1/devices?page=12",
    "prev":  null,
    "next":  "https://.../api/v1/devices?page=2"
  },
  "meta": { "current_page": 1, "from": 1, "to": 50, "per_page": 50, "total": 593 },
  "next_page_url": "https://.../api/v1/devices?page=2"
}

The client's async iterators consume this envelope and yield individual rows from data, then follow next_page_url until exhausted.

A typical Device object:

{
  "id": 42,
  "name": "core-switch-01",
  "ip_address": "10.0.0.1",
  "snmp_version": "2c",
  "device_type": "cisco_ios",
  "status": "up",                    // up | down | unknown
  "last_polled_at": "2026-04-22T10:14:33Z",
  "poll_interval_seconds": 60,
  "is_active": true,
  "device_group": { "id": 3, "name": "Core network", "color": "#3b82f6" }
}

A typical Monitor object:

{
  "id": 117,
  "name": "Public website",
  "type": "https",
  "host": "example.com",
  "port": 443,
  "status": "up",                    // up | down | degraded | unknown
  "response_time_ms": 142,
  "ssl_expiry_at": "2026-09-14T00:00:00Z",
  "uptime_percent_7d": 99.97,
  "uptime_percent_30d": 99.81,
  "consecutive_failures": 0,
  "last_checked_at": "2026-04-22T10:15:01Z",
  "config": { "method": "GET", "path": "/", "expected_status": 200, "ssl_verify": true }
}

A typical AlertEvent:

{
  "id": 9821,
  "severity": "critical",            // info | warning | critical
  "status": "open",                  // open | acknowledged | resolved
  "triggered_at": "2026-04-22T09:55:12Z",
  "acknowledged_at": null,
  "resolved_at": null,
  "current_value": "down",
  "monitor": { "id": 117, "name": "Public website", "type": "https" },
  "device":  null
}

Pagination & async iterators

The two-line loop:

for await (const device of api.devices()) {
    console.log(device.id, device.name);
}

…is equivalent to:

let page = 1;
while (true) {
    const res = await fetch(`${baseUrl}/api/v1/devices?page=${page}`, ...);
    for (const d of res.data) console.log(d.id, d.name);
    if (!res.next_page_url) break;
    page++;
}

You can pass any query string to the iterator — page, per_page, search, sort, and filter[*] are all forwarded:

for await (const m of api.monitors({ per_page: 100, sort: '-response_time_ms' })) {
    if (m.response_time_ms > 1000) console.warn(`${m.name} is slow: ${m.response_time_ms}ms`);
}

To collect into an array instead of streaming:

const downDevices = [];
for await (const d of api.devices({ 'filter[status]': 'down' })) {
    downDevices.push(d);
}

Filtering, sorting, searching

Query param Example Effect
search search=core Free-text search across name/host fields
filter[<field>] filter[status]=down Exact-match filter (chainable)
sort sort=-last_polled_at Sort column. Prefix - for descending.
per_page per_page=100 Page size (cap depends on endpoint)
page page=3 Start page (use the iterator instead unless paging manually)

Combine freely:

for await (const ev of api.alertEvents({
    'filter[severity]': 'critical',
    'filter[status]':   'open',
    'sort':             '-triggered_at',
    'per_page':         50,
})) {
    handle(ev);
}

Working with monitors

Create:

const monitor = await api.createMonitor({
    name: 'Login API health',
    type: 'https',
    host: 'api.example.com',
    port: 443,
    check_interval_seconds: 60,
    timeout_ms: 5000,
    config: {
        method: 'GET',
        path: '/health',
        expected_status: 200,
        expected_body_regex: '"ok":true',
        ssl_verify: true,
        alert_ssl_expiry_days: 14,
    },
});
console.log(`Created monitor ${monitor.id}`);

Other supported type values: ping, tcp, http, https, ssh, smtp, ftp, pop3, imap, dns, mysql, redis, snmp. Each type has its own config schema — see the main project README.

Force an immediate re-check:

await api.checkMonitor(monitor.id);   // returns { queued: true, job_id: '...' }

The check runs asynchronously on the probe service. Poll monitor.status or monitorResults(id) to see the outcome.

Read the last N results:

const recent = await api.monitorResults(monitor.id, { per_page: 20 });
for (const r of recent.data) {
    console.log(r.checked_at, r.status, `${r.response_time_ms} ms`);
}

Uptime stats for SLA reporting:

const uptime = await api.monitorUptime(monitor.id, { window: '30d' });
// { uptime_percent: 99.91, total_checks: 43200, failed_checks: 39, ... }

Working with alerts

// Stream every open critical alert
for await (const ev of api.alertEvents({
    'filter[status]':   'open',
    'filter[severity]': 'critical',
})) {
    await api.acknowledgeAlertEvent(ev.id, `Auto-ack from ${process.env.HOSTNAME}`);
}

// Mark resolved when your downstream system resolves it
await api.resolveAlertEvent(ev.id, 'Closed by INC-9421');

note is optional on both ack and resolve — pass a short string and it shows up in the audit log for that event.

Error handling

Any non-2xx response throws AcsMonApiError with the HTTP status and parsed body attached. Network errors (DNS failure, TLS failure, timeout) throw with status = 0.

import { AcsMonApiError } from './src/client.mjs';

try {
    await api.pollDevice(99999);
} catch (err) {
    if (err instanceof AcsMonApiError) {
        console.error(`HTTP ${err.status}:`, err.body);
        if (err.status === 404) return;            // gone, skip
        if (err.status === 403) throw new Error('Token lacks devices.edit permission');
        if (err.status === 0)   console.warn('Network problem, will retry');
    }
    throw err;
}
Status Meaning Recovery
401 Token missing/expired Call login() again or refresh the secret
403 Authenticated but lacks the permission slug for that route Grant the role via the UI
404 Resource doesn't exist (or wrong ID) Check the ID, no retry
422 Validation error — see err.body.errors Fix the payload
429 Rate-limited Back off (the server returns Retry-After)
500+ Server bug or upstream outage Retry with exponential backoff
0 Network/DNS/TLS/timeout Retry with exponential backoff

A simple retry helper:

async function withRetry(fn, { attempts = 4, baseMs = 500 } = {}) {
    for (let i = 0; i < attempts; i++) {
        try { return await fn(); }
        catch (err) {
            const retriable = err instanceof AcsMonApiError
                && (err.status === 0 || err.status === 429 || err.status >= 500);
            if (!retriable || i === attempts - 1) throw err;
            await new Promise(r => setTimeout(r, baseMs * 2 ** i));
        }
    }
}

await withRetry(() => api.pollDevice(42));

Custom fetch options

The client uses native fetch. To inject custom behaviour (proxy, mTLS, custom CA, custom headers) wrap or subclass AcsMonitorClient, or pre-set globals via undici:

import { setGlobalDispatcher, ProxyAgent } from 'undici';

setGlobalDispatcher(new ProxyAgent('http://corp-proxy:3128'));
const api = new AcsMonitorClient('https://monitoring.example.com');

To accept a self-signed cert in a lab environment (don't do this in production):

import { setGlobalDispatcher, Agent } from 'undici';
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }));

To override the request timeout per call, construct a second client:

const slowOps = new AcsMonitorClient(baseUrl, token, { timeoutMs: 120000 });
await slowOps.pollDevice(42);   // poll can take a while on big devices

Token storage best practice

  • Server-side daemons: read ACSMON_TOKEN from environment, never commit it. Store in your secrets manager (HashiCorp Vault, AWS Secrets Manager, Doppler, Infisical).
  • CLI tools: read from ~/.config/acsmon/token with 0600 permissions, falling back to process.env.ACSMON_TOKEN.
  • Serverless / edge: inject via the platform's secret store (Cloudflare secrets, Vercel env vars, AWS Lambda env).
  • Never put the token into a public Git repo, browser-side bundle, or client-side JS — it grants the same access as the user account it was issued to.

TypeScript usage

The client is plain ESM JavaScript with JSDoc, so TypeScript projects can import it directly with allowJs enabled, or you can declare types:

declare module './lib/acsmon.mjs' {
    export class AcsMonitorClient {
        constructor(baseUrl: string, token?: string | null, opts?: { timeoutMs?: number });
        login(email: string, password: string): Promise<string>;
        logout(): Promise<void>;
        devices(query?: Record<string, string | number>): AsyncIterableIterator<any>;
        device(id: number): Promise<any>;
        pollDevice(id: number): Promise<any>;
        // … and so on
    }
    export class AcsMonApiError extends Error {
        status: number;
        body: any;
    }
}

For a fully typed API surface you can also generate one from your ACS Monitor's OpenAPI export (Settings → API → OpenAPI spec).

Recipes

Sync devices into a CMDB

import { AcsMonitorClient } from './src/client.mjs';

const api = new AcsMonitorClient(process.env.ACSMON_BASE_URL, process.env.ACSMON_TOKEN);
const upserts = [];

for await (const d of api.devices()) {
    upserts.push({
        external_id: `acsmon-${d.id}`,
        hostname:    d.name,
        ip:          d.ip_address,
        status:      d.status,
        last_seen:   d.last_polled_at,
    });
}

await myCmdb.bulkUpsert(upserts);
console.log(`Synced ${upserts.length} devices`);

Forward open criticals into PagerDuty

import { AcsMonitorClient } from './src/client.mjs';

const api = new AcsMonitorClient(process.env.ACSMON_BASE_URL, process.env.ACSMON_TOKEN);

for await (const ev of api.alertEvents({
    'filter[status]':   'open',
    'filter[severity]': 'critical',
})) {
    await fetch('https://events.pagerduty.com/v2/enqueue', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            routing_key: process.env.PAGERDUTY_KEY,
            event_action: 'trigger',
            dedup_key: `acsmon-${ev.id}`,
            payload: {
                summary: `${ev.monitor?.name ?? ev.device?.name}: ${ev.current_value}`,
                severity: 'critical',
                source: 'ACS Monitor',
                custom_details: ev,
            },
        }),
    });
    await api.acknowledgeAlertEvent(ev.id, 'Forwarded to PagerDuty');
}

CI/CD post-deploy verification

// fail the pipeline if the production health monitor isn't green within 5 min
import { AcsMonitorClient } from './src/client.mjs';

const api = new AcsMonitorClient(process.env.ACSMON_BASE_URL, process.env.ACSMON_TOKEN);
const monitorId = Number(process.env.HEALTH_MONITOR_ID);

await api.checkMonitor(monitorId);

const deadline = Date.now() + 5 * 60 * 1000;
while (Date.now() < deadline) {
    const m = await api.monitor(monitorId);
    if (m.status === 'up') {
        console.log(`✓ ${m.name} is up (${m.response_time_ms}ms)`);
        process.exit(0);
    }
    await new Promise(r => setTimeout(r, 10_000));
}
console.error('Health monitor did not return to "up" within 5 minutes');
process.exit(1);

Weekly uptime report

import { AcsMonitorClient } from './src/client.mjs';

const api = new AcsMonitorClient(process.env.ACSMON_BASE_URL, process.env.ACSMON_TOKEN);
const rows = [];

for await (const m of api.monitors()) {
    const u = await api.monitorUptime(m.id, { window: '7d' });
    rows.push({ name: m.name, uptime_pct: u.uptime_percent, failed: u.failed_checks });
}

rows.sort((a, b) => a.uptime_pct - b.uptime_pct);
console.table(rows);

Troubleshooting

AcsMonApiError ... → HTTP 401 — The token is missing, expired, or belongs to a deleted user. Re-issue it from Settings → Users.

AcsMonApiError ... → HTTP 403 — Authentication worked but the user/role doesn't carry the permission slug for that endpoint. The endpoint table in the project's main CLAUDE.md lists each route's required permission slug.

AbortError: This operation was aborted — Hit the per-request timeout (default 30s). Construct the client with a higher timeoutMs, or check whether the API server is overloaded.

fetch failed / ENOTFOUND / ECONNREFUSED — Network or DNS problem reaching ACSMON_BASE_URL. Verify the URL with curl -v "$ACSMON_BASE_URL/api/v1/system/health".

SELF_SIGNED_CERT_IN_CHAIN — Your install uses a self-signed TLS cert. Either install a real cert, add the CA to Node's trust store (NODE_EXTRA_CA_CERTS=/path/to/ca.pem), or use the undici override shown above (lab only).

Iterator yields nothing — The query returned no data. Check the filter values and confirm the user has *.view on that resource.

SyntaxError: Cannot use import statement outside a module — Your project is CommonJS. Either rename the consumer to .mjs, set "type": "module" in package.json, or use dynamic import: const { AcsMonitorClient } = await import('./client.mjs');.


© Anglia Computer Solutions Ltd. — ACS Monitor is a product of Anglia Computer Solutions. Visit acsmon.com for documentation, demos and licensing.

About

Official Node.js 18+ client for the ACS Monitor REST API — zero dependencies, uses native fetch.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors