UNPKG

custom-metrics

Version:
1,085 lines (1,084 loc) 41.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomMetrics = exports.DefaultSpans = void 0; const process_1 = __importDefault(require("process")); const client_dynamodb_1 = require("@aws-sdk/client-dynamodb"); const util_dynamodb_1 = require("@aws-sdk/util-dynamodb"); const Version = 1; const Assert = false; const Buffering = true; const DefaultResolution = 0; const MaxSeq = Number.MAX_SAFE_INTEGER; const MaxRetries = 10; const MetricListLimit = 10000; exports.DefaultSpans = [ { period: 5 * 60, samples: 10 }, { period: 60 * 60, samples: 12 }, { period: 24 * 60 * 60, samples: 12 }, { period: 7 * 24 * 60 * 60, samples: 14 }, { period: 28 * 24 * 60 * 60, samples: 14 }, { period: 365 * 24 * 60 * 60, samples: 12 }, ]; var Instances = {}; process_1.default.on('SIGTERM', async () => { await CustomMetrics.terminate(); }); class CustomMetrics { constructor(options = {}) { this.consistent = false; this.buffers = null; this.prefix = 'metric'; this.log = new Log(options.log); if (options.ttl && typeof options.ttl != 'number') { throw new Error('Bad type for "ttl" option'); } if (options.spans && (!Array.isArray(options.spans) || options.spans.length == 0)) { throw new Error('The "spans" option must be an non-empty array'); } if (options.source && typeof options.source != 'string') { throw new Error('Non-string "source" option'); } if (options.pResolution != undefined && (options.pResolution < 0 || options.pResolution > 1000)) { throw new Error('Invalid "pResolution" option. Must be between 0 and 1000. Default is 0'); } if (options.consistent != null && typeof options.consistent != 'boolean') { throw new Error('Bad type for "consistent" option'); } if (options.prefix) { this.prefix = options.prefix; } if (options.buffer) { if (typeof options.buffer != 'object') { throw new Error('Bad type for "buffer" option'); } this.buffer = options.buffer; } this.expires = options.expires || 'expires'; this.primaryKey = options.primaryKey || 'pk'; this.sortKey = options.sortKey || 'sk'; this.type = options.type || { _type: 'Metric' }; if (options.client) { this.client = options.client; } else { let params = {}; if (options.creds) { params.credentials = options.creds; params.region = params.credentials.region; } if (options.region) { params.region = options.region; } this.client = new client_dynamodb_1.DynamoDBClient(params); } if (!options.table) { throw new Error('Missing DynamoDB table name property'); } this.table = options.table; this.options = options; this.owner = options.owner || 'default'; this.spans = options.spans || exports.DefaultSpans; this.ttl = options.ttl || this.spans[this.spans.length - 1].period; if (options.consistent != null) { this.consistent = options.consistent; } if (options.source) { this.source = options.source; } this.pResolution = options.pResolution || DefaultResolution; } async emit(namespace, metricName, value, dimensionsList = [{}], options = {}) { if (value == undefined || value == null) { throw new Error('Invalid metric value'); } if (dimensionsList.length == 0) { dimensionsList = [{}]; } value = Number(value); if (isNaN(value)) { throw new Error(`Value to emit is not valid`); } if (!namespace || !metricName) { throw new Error('Missing emit namespace / metric argument'); } if (!Array.isArray(dimensionsList)) { throw new Error('Dimensions must be an array'); } if (dimensionsList.length == 0) { dimensionsList = [{}]; } let point; point = { count: 1, sum: value }; return await this.emitDimensions(namespace, metricName, point, dimensionsList, options); } async emitDimensions(namespace, metricName, point, dimensionsList, options) { let result; for (let dim of dimensionsList) { let dimensions = this.makeDimensionString(dim); let buffer = options.buffer || this.buffer; if (buffer && (buffer.elapsed || buffer.force || buffer.sum || buffer.count) && Buffering) { result = await this.bufferMetric(namespace, metricName, point, dimensions, options); } else { result = await this.emitDimensionedMetric(namespace, metricName, point, dimensions, options); } } return result; } async bufferMetric(namespace, metricName, point, dimensions, options) { let buffer = options.buffer || this.buffer; let key = this.getBufferKey(namespace, metricName, dimensions); let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); let owner = options.owner || this.owner; let elt = await this.getBufferedMetric(owner, namespace, metricName, dimensions, options); if (buffer.force || (buffer.sum && elt.sum + point.sum >= buffer.sum) || (buffer.count && elt.count + point.count >= buffer.count) || timestamp >= elt.timestamp) { let metric = await this.emitDimensionedMetric(namespace, metricName, { count: elt.count, sum: elt.sum, timestamp: elt.timestamp }, dimensions, Object.assign({}, options, { timestamp: timestamp * 1000 })); elt.metric = metric; elt.count = elt.sum = 0; elt.timestamp = timestamp + (buffer.elapsed || this.spans[0].period / this.spans[0].samples); } elt.count += point.count; elt.sum += point.sum; this.addValue(elt.metric.spans, point, timestamp); CustomMetrics.saveInstance({ key }, this); return elt.metric; } async getBufferedMetric(owner, namespace, metricName, dimensions, options) { let buffers = (this.buffers = this.buffers || {}); let key = this.getBufferKey(namespace, metricName, dimensions); let elt = buffers[key]; if (elt) { return elt; } let buffer = options.buffer || this.buffer; let elapsed = buffer.elapsed || this.spans[0].period / this.spans[0].samples; let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); let metric = await this.getMetric(owner, namespace, metricName, dimensions, options.log); if (!metric) { metric = this.initMetric(owner, namespace, metricName, dimensions, timestamp); } elt = { count: 0, sum: 0, timestamp: timestamp + elapsed, elapsed: elapsed, namespace: namespace, metricName: metricName, metric: metric, dimensions, }; buffers[key] = elt; return elt; } async emitDimensionedMetric(namespace, metricName, point, dimensions, options = {}) { let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); let ttl = options.ttl != undefined ? options.ttl : this.ttl; let retries = MaxRetries; let metric; let backoff = 10; let chan = options.log == true ? 'info' : 'trace'; do { let owner = options.owner || this.owner; metric = await this.getMetric(owner, namespace, metricName, dimensions, options.log); if (metric) { if (options.upgrade) { metric = this.upgradeMetric(metric); } } else { metric = this.initMetric(owner, namespace, metricName, dimensions, timestamp); } this.addValue(metric.spans, point, timestamp); if (this.source) { metric._source = this.source; } if (ttl) { metric.expires = timestamp + ttl; } if (await this.putMetric(metric, options)) { break; } if (retries == 0) { this.log.error(`Metric update has too many retries`, { namespace, metricName, dimensions }); break; } this.log[chan](`Retry ${MaxRetries - retries} metric update ${metric.namespace} ${metric.metric} ${metric.dimensions}`, { retries, metric, }); backoff = backoff * 2; this.log[chan](`Retry backoff ${backoff} ${this.jitter(backoff)}`); await this.delay(this.jitter(backoff)); } while (retries-- > 0); return metric; } async upgrade(namespace, metricName, dimensionsList = [{}], options = {}) { let owner = options.owner || this.owner; if (dimensionsList.length == 0) { dimensionsList = [{}]; } let metric; for (let dim of dimensionsList) { let dimensions = this.makeDimensionString(dim); let old = await this.getMetric(owner, namespace, metricName, dimensions, options.log); metric = this.upgradeMetric(old); await this.putMetric(metric, options); } return metric; } upgradeMetric(old) { let required = false; if (this.spans.length == old.spans.length) { for (let [index, span] of Object.entries(old.spans)) { if (span.period != this.spans[index].period || span.samples != this.spans[index].samples) { required = true; } } if (!required) { return old; } } let timestamp = Math.min(...old.spans.map((span) => span.end - span.period)) || Math.floor(Date.now() / 1000); let metric = this.initMetric(old.owner, old.namespace, old.metric, old.dimensions, timestamp); for (let span of old.spans) { let interval = span.period / span.samples; let start = span.end - span.points.length * interval; let si = metric.spans.findIndex((s) => s.end - s.period <= start || s.end <= start); for (let point of span.points) { this.addValue(metric.spans, point, start, si); start += interval; } } return metric; } static async terminate() { await CustomMetrics.flushAll(); } static async flushAll() { for (let [key, instance] of Object.entries(Instances)) { await instance.flush(); CustomMetrics.freeInstanceByKey(key); } Instances = {}; } async flush(options = {}) { if (!this.buffers) return; let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); for (let elt of Object.values(this.buffers)) { await this.flushElt(elt, timestamp); } } async flushElt(elt, timestamp) { elt.timestamp = Math.min(timestamp, elt.timestamp); let metric = await this.emitDimensionedMetric(elt.namespace, elt.metricName, elt, elt.dimensions, { timestamp: elt.timestamp * 1000, }); elt.count = elt.sum = 0; elt.metric = metric; elt.timestamp = timestamp + (elt.elapsed || this.spans[0].period / this.spans[0].samples); } getBufferKey(namespace, metricName, dimensions) { return `${namespace}|${metricName}|${JSON.stringify(dimensions)}`; } async query(namespace, metricName, dimensions, period, statistic, options = {}) { let owner = options.owner || this.owner; let dimString = this.makeDimensionString(dimensions); if (period > this.spans.at(-1).period) { period = this.spans.at(-1).period; } let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); if (this.buffers) { let key = this.getBufferKey(namespace, metricName, dimString); if (this.buffers[key]) { await this.flushElt(this.buffers[key], timestamp); } } let metric = await this.getMetric(owner, namespace, metricName, dimString, options.log); if (!metric) { return { dimensions, id: options.id, metric: metricName, namespace, period, points: [], owner, samples: 0 }; } let result = this.processMetric(metric, period, statistic, timestamp, options); this.log[options.log == true ? 'info' : 'trace'](`Query metrics ${namespace}, ${metricName}`, { dimensions, period, statistic, options, result, }); return result; } async queryMetrics(namespace, metric, period, statistic, options = {}) { let owner = options.owner || this.owner; let next = options.next; let limit = options.limit || MetricListLimit; let chan = options.log == true ? 'info' : 'trace'; let items, command; let count = 0; do { ; ({ command, items, next } = await this.findMetrics(owner, namespace, metric, limit, next, 'spans')); this.log[chan](`Find metrics ${namespace}, ${metric}`, { command, items }); if (items.length) { count += items.length; } } while (next && count < limit); let timestamp = Math.floor((options.timestamp || Date.now()) / 1000); let results = []; for (let metric of items) { let result = this.processMetric(metric, period, statistic, timestamp, { accumulate: true, timestamp }); results.push(result); } return results; } processMetric(metric, period, statistic, timestamp, options) { let end; let si; let owner = options.owner || this.owner; if (options.start) { let start = options.start / 1000; si = metric.spans.findIndex((s) => period <= s.period && s.end - s.period <= start && start < s.end); end = start + period; } else { let span = metric.spans[0]; let interval = span.period / span.samples; if (span.end - interval <= timestamp && timestamp < span.end) { end = span.end; } else { end = timestamp; } si = metric.spans.findIndex((s) => period <= s.period); } if (si < 0) { si = metric.spans.length - 1; } if (statistic == 'current' && options.accumulate) { si = 0; } this.addValue(metric.spans, { count: 0, sum: 0 }, timestamp, 0, si); let span = metric.spans[si]; let result; if (options.accumulate) { result = this.accumulateMetric(metric, span, statistic, owner, end, period); } else { result = this.calculateSeries(metric, span, statistic, owner, end, period); } result.id = options.id; return result; } accumulateMetric(metric, span, statistic, owner, end, period) { let start = this.alignTime(span, end - period); let value = 0, count = 0, pvalues = []; if (statistic == 'max') { value = Number.NEGATIVE_INFINITY; } else if (statistic == 'min') { value = Infinity; } else if (statistic == 'sum') { value = 0; count = 0; } else if (statistic == 'count') { value = 0; count = 0; } else if (statistic == 'current') { value = 0; count = 0; } else if (statistic.match(/^p[0-9]+/)) { pvalues = []; } else { value = 0; count = 0; } let points = span.points; let interval = span.period / span.samples; let t = span.end - span.points.length * interval; if (statistic == 'current') { for (let s of metric.spans) { for (let p of s.points.reverse()) { if (p.count > 0) { value = p.sum / p.count; count = p.count; break; } } if (count > 0) break; } } else { for (let i = 0; i < points.length; i++) { let point = points[i]; if (start <= t && t < start + period) { if (statistic == 'max') { if (point.max != undefined) { value = Math.max(value, point.max); } else { value = Math.max(value, point.sum / (point.count || 1)); } } else if (statistic == 'min') { if (point.min != undefined) { value = Math.min(value, point.min); } else { value = Math.min(value, point.sum / (point.count || 1)); } } else if (statistic == 'sum') { value += point.sum; } else if (statistic == 'count') { value += point.count; } else if (statistic.match(/^p[0-9]+/)) { pvalues = pvalues.concat(point.pvalues); } else { value += point.sum; } count += point.count; } t += interval; } } if (statistic.match(/^p[0-9]+/)) { let p = parseInt(statistic.slice(1)); pvalues.sort((a, b) => a - b); let nth = Math.min(Math.round((pvalues.length * p) / 100 + 1), pvalues.length - 1); value = pvalues[nth]; } else if (statistic == 'avg') { value /= Math.max(count, 1); } return { dimensions: this.makeDimensionObject(metric.dimensions), metric: metric.metric, namespace: metric.namespace, owner: owner, period: span.period, points: [{ value, timestamp: start + period, count }], samples: span.samples, }; } calculateSeries(metric, span, statistic, owner, end, period) { let points = []; let interval = span.period / span.samples; let start = this.alignTime(span, end - period); let firstPoint = span.end - span.points.length * interval; let t; for (t = start; t < firstPoint && points.length < span.samples; t += interval) { points.push({ value: 0, count: 0, timestamp: t * 1000 }); } t = firstPoint; for (let point of span.points) { if (start <= t && t < end) { let value = undefined; if (point.count > 0) { if (statistic == 'max') { if (point.max != undefined) { if (value == undefined) { value = point.max; } else { value = Math.max(value, point.max); } } } else if (statistic == 'min') { if (point.min != undefined) { if (value == undefined) { value = point.min; } else { value = Math.min(value, point.min); } } } else if (statistic == 'sum') { value = point.sum; } else if (statistic == 'count') { value = point.count; } else if (statistic.match(/^p[0-9]+/)) { let p = parseInt(statistic.slice(1)); let pvalues = point.pvalues; pvalues.sort((a, b) => a - b); let nth = Math.min(Math.round((pvalues.length * p) / 100 + 1), pvalues.length - 1); value = pvalues[nth]; } else { value = point.sum / point.count; } } else { value = 0; } let timestamp = Math.min(t + interval, end) * 1000; points.push({ value, count: point.count, timestamp }); } t += interval; } let count = Math.min(Math.ceil(period / interval), span.samples); while (points.length < count) { let timestamp = Math.min(t + interval, end) * 1000; points.push({ value: 0, count: 0, timestamp }); t += interval; } return { dimensions: this.makeDimensionObject(metric.dimensions), metric: metric.metric, namespace: metric.namespace, period: span.period, points: points, owner: owner, samples: span.samples, }; } makeDimensionString(dimensions) { let result = []; let entries = Object.entries(dimensions).sort((a, b) => a[0].localeCompare(b[0])); for (let [name, value] of entries) { result.push(`${name}=${value}`); } return result.join(','); } makeDimensionObject(dimensions) { let result = {}; for (let dimension of dimensions.split(',')) { if (dimension) { let [key, value] = dimension.split('='); result[key] = value; } } return result; } addValue(spans, point, timestamp, si = 0, queryIndex = -1) { this.assert(spans); this.assert(timestamp); this.assert(0 <= si && si < spans.length); let span = spans[si]; let interval = span.period / span.samples; let points = span.points || []; while (points.length > span.samples) { points.shift(); } let start = span.end - points.length * interval; let shift = 0; if (points.length) { if (si < queryIndex && si + 1 < spans.length) { shift = points.length; } else if (timestamp >= start) { shift = Math.floor((timestamp - start) / interval) - span.samples; if (point.count && timestamp >= span.end) { shift += 1; } } shift = Math.max(0, Math.min(shift, points.length)); } if (shift > 0) { let t = start; for (let i = 0; i < shift; i++) { let p = points.shift(); if (p.count && si < spans.length - 1) { p.timestamp = t; this.addValue(spans, p, timestamp, si + 1, queryIndex); } t += interval; } } if (queryIndex >= 0 && si < queryIndex) { this.addValue(spans, point, timestamp, si + 1, queryIndex); return; } if (si < spans.length - 1) { if (point.timestamp) { let elapsed = (timestamp - point.timestamp); let target = spans.findIndex(s => s.period >= elapsed); if (target > si) { this.addValue(spans, point, timestamp, si + 1, queryIndex); return; } } if ((si + 1) < spans.length - 1) { this.addValue(spans, { count: 0, sum: 0 }, timestamp, si + 1, queryIndex); } } let index = this.updateSpan(span, point, timestamp); if (point.count && index >= 0) { this.setPoint(span, index, point); } } updateSpan(span, point, timestamp) { let interval = span.period / span.samples; let points = span.points || []; let start = span.end - points.length * interval; let when = point.timestamp || timestamp; let index; if (points.length == 0) { if (point.count) { points.push({ count: 0, sum: 0 }); } span.end = this.alignTime(span, when + 1); index = 0; } else { if (when < span.end - span.period) { return -1; } while (when < start) { points.unshift({ count: 0, sum: 0 }); start -= interval; } while (when >= span.end && points.length < span.samples) { points.push({ count: 0, sum: 0 }); span.end += interval; } index = Math.floor((when - start) / interval); } if (points.length > span.samples) { points = points.slice(-span.samples); } this.assert(0 <= index && index < points.length); return index; } setPoint(span, index, add) { let points = span.points; this.assert(0 <= index && index < points.length); let point = points[index]; if (!point) { this.log.error(`Metric null point`, { span, index, add }); return; } if (add.count) { let value = add.sum / add.count; if (point.min == undefined) { point.min = value; } else { point.min = Math.min(value, point.min); } if (point.max == undefined) { point.max = value; } else { point.max = Math.max(value, point.max); } } if (this.pResolution) { point.pvalues = point.pvalues || []; if (add.pvalues) { point.pvalues.push(...add.pvalues); } else { point.pvalues.push(add.sum / add.count); } point.pvalues.splice(0, point.pvalues.length - this.pResolution); } point.sum += add.sum; point.count += add.count; } async getMetricList(namespace = undefined, metric = undefined, options = { limit: MetricListLimit }) { let map = {}; let owner = options.owner || this.owner; let next = options.next; let limit = options.limit || MetricListLimit; let chan = options.log == true ? 'info' : 'trace'; let items, command; let count = 0; do { ; ({ command, items, next } = await this.findMetrics(owner, namespace, metric, limit, next)); this.log[chan](`Find metrics namespace: ${namespace}, metric: ${metric}`, { command, items }); if (items.length) { for (let item of items) { let ns = (map[item.namespace] = map[item.namespace] || {}); let met = (ns[item.metric] = ns[item.metric] || []); met.push(item.dimensions); } count += items.length; } } while (next && count < limit); let result = { namespaces: Object.keys(map) }; if (namespace && map[namespace]) { result.metrics = Object.keys(map[namespace]); if (metric) { let dimensions = map[namespace][metric]; if (dimensions) { result.dimensions = []; dimensions = dimensions.sort().filter((v, index, self) => self.indexOf(v) === index); for (let dimension of dimensions) { result.dimensions.push(this.makeDimensionObject(dimension)); } } } } return result; } initMetric(owner, namespace, name, dimensions, timestamp) { let metric = { dimensions, metric: name, namespace, owner, spans: [], version: Version, }; for (let sdef of this.spans) { let span = { samples: sdef.samples, period: sdef.period, end: null, points: [], }; let interval = span.period / span.samples; span.end = this.alignTime(span, timestamp + interval - 1); metric.spans.push(span); } return metric; } async getMetric(owner, namespace, metric, dimensions, log) { let command = new client_dynamodb_1.GetItemCommand({ TableName: this.table, Key: { [this.primaryKey]: { S: `${this.prefix}#${Version}#${owner}` }, [this.sortKey]: { S: `${this.prefix}#${namespace}#${metric}#${dimensions}` }, }, ConsistentRead: this.consistent, }); let data = await this.client.send(command); let result = null; if (data && data.Item) { let item = (0, util_dynamodb_1.unmarshall)(data.Item); result = this.mapItemFromDB(item); } if (log == true) { let chan = log == true ? 'info' : 'trace'; this.log[chan](`GetMetric ${namespace}, ${metric} ${dimensions}`, { cmd: command, result }); } return result; } async findMetrics(owner, namespace, metric, limit, startKey, fields = '') { let key = [namespace]; if (metric) { key.push(metric); } let start = startKey ? (0, util_dynamodb_1.marshall)(startKey) : undefined; let project = `${this.primaryKey}, ${this.sortKey}`; if (fields) { project += `, ${fields}`; } let command = new client_dynamodb_1.QueryCommand({ TableName: this.table, ExpressionAttributeNames: { '#_0': this.primaryKey, '#_1': this.sortKey, }, ExpressionAttributeValues: { ':_0': { S: `${this.prefix}#${Version}#${owner}` }, ':_1': { S: `${this.prefix}#${key.join('#')}` }, }, KeyConditionExpression: '#_0 = :_0 and begins_with(#_1, :_1)', ConsistentRead: this.consistent, Limit: limit, ScanIndexForward: true, ExclusiveStartKey: start, ProjectionExpression: project, }); let result = await this.client.send(command); let items = []; if (result.Items) { for (let i = 0; i < result.Items.length; i++) { let item = (0, util_dynamodb_1.unmarshall)(result.Items[i]); items.push(this.mapItemFromDB(item)); } } let next = undefined; if (result.LastEvaluatedKey) { next = (0, util_dynamodb_1.unmarshall)(result.LastEvaluatedKey); } return { items, next, command }; } async putMetric(item, options) { let ConditionExpression, ExpressionAttributeValues; let seq; if (item.seq != undefined) { seq = item.seq = item.seq || 0; if (item.seq++ >= MaxSeq) { item.seq = 0; } ConditionExpression = `seq = :_0`; ExpressionAttributeValues = { ':_0': { N: seq.toString() } }; } else { item.seq = 0; } let mapped = this.mapItemToDB(item); let params = { TableName: this.table, ReturnValues: 'NONE', Item: (0, util_dynamodb_1.marshall)(mapped, { removeUndefinedValues: true }), ConditionExpression, ExpressionAttributeValues, }; let command = new client_dynamodb_1.PutItemCommand(params); let chan = options.log == true ? 'info' : 'trace'; this.log[chan](`Put metric ${item.namespace}, ${item.metric}`, { dimensions: item.dimensions, command, params, item, }); try { await this.client.send(command); return true; } catch (err) { ; (function (err, log) { let code = err.code || err.name; if (code == 'ConditionalCheckFailedException') { log.trace(`Update collision`, { err }); } else if (code == 'ProvisionedThroughputExceededException') { log.info(`Provisioned throughput exceeded: ${err.message}`, { err, cmd: command, item }); } else { log.error(`Emit exception code ${err.name} ${err.code} message ${err.message}`, { err, cmd: command, item, }); throw err; } return false; })(err, this.log); } } mapItemFromDB(data) { let pk = data[this.primaryKey]; let sk = data[this.sortKey]; let owner = pk.split('#').pop(); let [, namespace, metric, dimensions] = sk.split('#'); let spans; if (data.spans) { spans = data.spans.map((s) => { return { end: s.se, period: s.sp, samples: s.ss, points: s.pt.map((p) => { let point = { count: Number(p.c), sum: Number(p.s) }; if (p.x != null) { point.max = Number(p.x); } if (p.m != null) { point.min = Number(p.m); } if (p.v) { point.pvalues = p.v; } return point; }), }; }); } let expires = data[this.expires]; let seq = data.seq; return { dimensions, expires, metric, namespace, owner, seq, spans }; } mapItemToDB(item) { let result = { [this.primaryKey]: `${this.prefix}#${Version}#${item.owner}`, [this.sortKey]: `${this.prefix}#${item.namespace}#${item.metric}#${item.dimensions}`, [this.expires]: item.expires, spans: item.spans.map((i) => { return { se: i.end, sp: i.period, ss: i.samples, pt: i.points.map((point) => { let p = { c: point.count, s: this.round(point.sum) }; if (point.max != null) { p.x = this.round(point.max); } if (point.min != null) { p.m = this.round(point.min); } if (point.pvalues) { p.v = point.pvalues; } return p; }), }; }), seq: item.seq, _source: item._source, }; if (this.type) { let [key, model] = Object.entries(this.type)[0]; result[key] = model; } return result; } formatDate(n) { function padTo2Digits(num) { return num.toString().padStart(2, '0'); } let date = new Date(n); const year = date.getFullYear().toString().slice(-2); const month = padTo2Digits(date.getMonth() + 1); const day = padTo2Digits(date.getDate()); const hours = padTo2Digits(date.getHours()); const minutes = padTo2Digits(date.getMinutes()); const seconds = padTo2Digits(date.getSeconds()); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } metricToString(metric) { let buf = []; buf.push(`${metric.namespace}/${metric.metric}/${JSON.stringify(metric.dimensions) || ''}`); for (let span of metric.spans) { let interval = span.period / span.samples; let start = span.end - span.points.length * interval; buf.push(` ${span.period} secs ${this.formatDate(start * 1000)} => ` + `${this.formatDate(span.end * 1000)} ${span.points.length} points`); for (let point of span.points) { buf.push(` count ${point.count} = sum ${point.sum}`); } } return buf.join('\n'); } queryToString(metric) { let points = metric.points.slice(0); let buf = []; buf.push(`${metric.namespace}/${metric.metric}/${JSON.stringify(metric.dimensions)} ${metric.period} ${points.length} points`); for (let point of points) { buf.push(` ${this.formatDate(point.timestamp || 0)} = ${point.value || '-'} / ${point.count}`); } return buf.join('\n'); } static freeInstanceByKey(key) { delete Instances[key]; } static saveInstance(tags, metrics) { let key = JSON.stringify(tags); Instances[key] = metrics; } alignTime(span, timestamp) { let interval = span.period / span.samples; return Math.ceil(timestamp / interval) * interval; } assert(c) { if (!c && Assert) { let msg = { stack: '' }; if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(msg); } else { msg.stack = new Error('Assert').stack; } this.log.error(`Assertion failed`, { stack: msg.stack }); } } info(message, context = {}) { console.log('INFO: ' + message, context); } error(message, context = {}) { console.log('ERROR: ' + message, context); } trace(message, context = {}) { console.log('TRACE: ' + message, context); } round(n) { if (isNaN(n) || n == null) { return 0; } let places = 16 - n.toFixed(0).length; return Number(n.toFixed(places)) - 0; } jitter(msecs) { return Math.min(10 * 1000, Math.floor(msecs / 2 + msecs * Math.random())); } async delay(time) { return new Promise(function (resolve, reject) { setTimeout(() => resolve(true), time); }); } } exports.CustomMetrics = CustomMetrics; class Log { constructor(dest) { this.senselogs = null; this.logger = null; this.verbose = false; if (dest === true) { this.logger = this.defaultLogger; } else if (dest == 'verbose') { this.logger = this.defaultLogger; this.verbose = true; } else if (dest && typeof dest.info == 'function') { this.senselogs = dest; } } error(message, context) { this.process('error', message, context); } info(message, context) { this.process('info', message, context); } trace(message, context) { this.process('trace', message, context); } process(chan, message, context) { if (this.logger) { this.logger(chan, message, context); } else if (this.senselogs) { this.senselogs[chan](message, context); } } defaultLogger(chan, message, context) { if (chan == 'trace' && !this.verbose) { return; } let tag = chan.toUpperCase(); if (context) { try { console.log(tag, message, JSON.stringify(context, null, 4)); } catch (err) { let buf = ['{']; for (let [key, value] of Object.entries(context)) { try { buf.push(` ${key}: ${JSON.stringify(value, null, 4)}`); } catch (err) { } } buf.push('}'); console.log(tag, message, buf.join('\n')); } } else { console.log(tag, message); } } }