-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathexporter.js
102 lines (90 loc) · 3.51 KB
/
exporter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
const { NS1Client, NS1Record } = require('./ns1');
const _ = require('lodash');
const SIXTY_SECONDS = 60000;
const GRANULARITY_RECORD = "record";
const GRANULARITY_ZONE = "zone";
const CLIENT_BATCH_SIZE = parseInt(process.env.CLIENT_BATCH_SIZE);
class NS1Exporter {
constructor(apiKey, zones = null, granularity = GRANULARITY_RECORD) {
this.apiKey = apiKey;
this.apiClient = new NS1Client(this.apiKey);
this.zones = zones;
this.granularity = granularity;
this.cache = {};
}
/**
* Initializes the exporter by loading metadata about zones and DNS records from the API
*/
async initExporter() {
console.log("Initializing exporter...");
this.zones = this.zones || await this.apiClient.listZones();
if(this.granularity === GRANULARITY_RECORD) {
for (const zone of this.zones) {
const records = await this.apiClient.listRecordsForZone(zone);
for (const record of records) {
this.cache[record.id] = record;
}
}
} else {
for(const zone of this.zones) {
const zoneRecord = await this.apiClient.getZone(zone);
this.cache[zoneRecord.id] = zoneRecord;
}
}
console.log(`Initialization completed, monitoring ${this.zones.length} zones \
${this.granularity === GRANULARITY_RECORD ? `that contain ${Object.keys(this.cache).length} records` : ''}`);
}
/**
* Returns an OpenMetrics-formatted string with metric data about all the monitored entities
* This is the core function of the exporter, which both invokes API calls, stores data into the in-memory
* cache to prevent excessive API calls and returns the data in a properly-formatted manner
* @returns {String} the body of the /metrics response
*/
async metrics() {
await this.updateCachedMetrics();
const updatedRecords = Object.values(this.cache);
const prometheusMetrics = updatedRecords.map((r) => r.asPrometheusMetric());
return [
"# TYPE ns1_record_queries_per_minute gauge",
...prometheusMetrics,
"# EOF"
].join("\n");
}
/**
* Updates the cached metric values for all monitored zones/records whose values have not been recently updated
*/
async updateCachedMetrics() {
const recordsToUpdate = Object.values(this.cache).filter(isMetricStale);
const recordBatches = _.chunk(recordsToUpdate, CLIENT_BATCH_SIZE);
for(const batch of recordBatches) {
const updatePromises = batch.map((r) => this.updateSingleRecordMetric(r));
await Promise.all(updatePromises);
}
}
/**
* Updates a single NS1Record instance by pulling the most recent value from the NS1 API
* @param record an NS1Record instance to update
*/
async updateSingleRecordMetric(record) {
const now = Date.now();
try {
const newValue = await this.apiClient.getRecordQps(record);
this.cache[record.id].lastValue = newValue.toFixed(3);
this.cache[record.id].lastTimestamp = Math.floor(now / SIXTY_SECONDS) * SIXTY_SECONDS;
} catch(error) {
console.log(`Failed to fetch record ${record.domain} (skipping): ${error.message}`);
}
}
}
/**
* A predicate function which returns true if the last timestamp of a given record is older than 1 minute
* @param record an NS1Record
* @returns {boolean} whether the record's metrics should be updated
*/
function isMetricStale(record) {
const now = Date.now();
const nowMinute = Math.floor(now / SIXTY_SECONDS);
const recordMinute = Math.floor(record.lastTimestamp / SIXTY_SECONDS);
return nowMinute > recordMinute;
}
module.exports = NS1Exporter;