diff --git a/examples/host-metrics/node.js b/examples/host-metrics/node.js index 682263578c..96734ab318 100644 --- a/examples/host-metrics/node.js +++ b/examples/host-metrics/node.js @@ -1,17 +1,24 @@ 'use strict'; const { HostMetrics } = require('@opentelemetry/host-metrics'); -const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); +// const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const { MeterProvider } = require('@opentelemetry/metrics'); +const { CollectorMetricExporter } = require('@opentelemetry/exporter-collector'); +const exporter = new CollectorMetricExporter({ + headers: {}, + serviceName: 'test-host-metrics', + // url: '', +}); -const exporter = new PrometheusExporter( - { - startServer: true, - }, - () => { - console.log('prometheus scrape endpoint: http://localhost:9464/metrics'); - }, -); +// for testing purposes if you don't want to use CollectorMetricExporter +// const exporter = new PrometheusExporter( +// { +// startServer: true, +// }, +// () => { +// console.log('prometheus scrape endpoint: http://localhost:9464/metrics'); +// }, +// ); const meterProvider = new MeterProvider({ exporter, @@ -20,3 +27,8 @@ const meterProvider = new MeterProvider({ const hostMetrics = new HostMetrics({ meterProvider, name: 'example-host-metrics' }); hostMetrics.start(); + +// keep running +(function wait () { + setTimeout(wait, 1000); +})(); diff --git a/examples/host-metrics/package.json b/examples/host-metrics/package.json index 0563175284..3a25f5b4db 100644 --- a/examples/host-metrics/package.json +++ b/examples/host-metrics/package.json @@ -26,10 +26,11 @@ "url": "https://github.com/open-telemetry/opentelemetry-js/issues" }, "dependencies": { - "@opentelemetry/api": "^0.12.0", - "@opentelemetry/core": "^0.12.0", - "@opentelemetry/exporter-prometheus": "^0.12.0", - "@opentelemetry/metrics": "^0.12.0", + "@opentelemetry/api": "^0.13.0", + "@opentelemetry/core": "^0.13.0", + "@opentelemetry/exporter-prometheus": "^0.13.0", + "@opentelemetry/exporter-collector": "^0.13.0", + "@opentelemetry/metrics": "^0.13.0", "@opentelemetry/host-metrics": "^0.11.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme" diff --git a/packages/opentelemetry-host-metrics/package.json b/packages/opentelemetry-host-metrics/package.json index 9127052aa1..359579f553 100644 --- a/packages/opentelemetry-host-metrics/package.json +++ b/packages/opentelemetry-host-metrics/package.json @@ -61,9 +61,9 @@ "typescript": "3.9.7" }, "dependencies": { - "@opentelemetry/api": "^0.12.0", - "@opentelemetry/core": "^0.12.0", - "@opentelemetry/metrics": "^0.12.0", - "systeminformation": "^4.27.10" + "@opentelemetry/api": "^0.13.0", + "@opentelemetry/core": "^0.13.0", + "@opentelemetry/metrics": "^0.13.0", + "systeminformation": "^4.31.0" } } diff --git a/packages/opentelemetry-host-metrics/src/BaseMetrics.ts b/packages/opentelemetry-host-metrics/src/BaseMetrics.ts new file mode 100644 index 0000000000..0ed0b88cb7 --- /dev/null +++ b/packages/opentelemetry-host-metrics/src/BaseMetrics.ts @@ -0,0 +1,125 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '@opentelemetry/api'; +import * as metrics from '@opentelemetry/metrics'; + +import { VERSION } from './version'; + +/** + * Metrics Collector Configuration + */ +export interface MetricsCollectorConfig { + logger?: api.Logger; + // maximum timeout to wait for stats collection default is 500ms + maxTimeoutUpdateMS?: number; + // Meter Provider + meterProvider?: metrics.MeterProvider; + // Character to be used to join metrics - default is "." + metricNameSeparator?: string; + // Name of component + name: string; + // metric export endpoint + url?: string; +} + +const DEFAULT_MAX_TIMEOUT_UPDATE_MS = 500; +const DEFAULT_NAME = 'opentelemetry-host-metrics'; +const DEFAULT_METRIC_NAME_SEPARATOR = '.'; + +// default label name to be used to store metric name +const DEFAULT_KEY = 'name'; + +/** + * Base Class for metrics + */ +export abstract class BaseMetrics { + protected _logger: api.Logger | undefined; + protected _maxTimeoutUpdateMS: number; + protected _meter: metrics.Meter; + private _name: string; + private _boundCounters: { [key: string]: api.BoundCounter } = {}; + private _metricNameSeparator: string; + + constructor(config: MetricsCollectorConfig) { + this._logger = config.logger || new api.NoopLogger(); + this._name = config.name || DEFAULT_NAME; + this._maxTimeoutUpdateMS = + config.maxTimeoutUpdateMS || DEFAULT_MAX_TIMEOUT_UPDATE_MS; + this._metricNameSeparator = + config.metricNameSeparator || DEFAULT_METRIC_NAME_SEPARATOR; + const meterProvider = + config.meterProvider! || api.metrics.getMeterProvider(); + if (!config.meterProvider) { + this._logger.warn('No meter provider, using default'); + } + this._meter = meterProvider.getMeter(this._name, VERSION); + } + + /** + * Creates a metric key name based on metric name and a key + * @param metricName + * @param key + */ + protected _boundKey(metricName: string, key: string) { + if (!key) { + return metricName; + } + return `${metricName}${this._metricNameSeparator}${key}`; + } + + /** + * Updates counter based on boundkey + * @param metricName + * @param key + * @param value + */ + protected _counterUpdate(metricName: string, key: string, value = 0) { + const boundKey = this._boundKey(metricName, key); + this._boundCounters[boundKey].add(value); + } + + /** + * @param metricName metric name - this will be added as label under name + * "name" + * @param values values to be used to generate bound counters for each + * value prefixed with metricName + * @param description metric description + */ + protected _createCounter( + metricName: string, + values: string[], + description?: string + ) { + const keys = values.map(key => this._boundKey(metricName, key)); + const counter = this._meter.createCounter(metricName, { + description: description || metricName, + }); + keys.forEach(key => { + this._boundCounters[key] = counter.bind({ [DEFAULT_KEY]: key }); + }); + } + + /** + * Creates metrics + */ + protected abstract _createMetrics(): void; + + /** + * Starts collecting stats + */ + public abstract start(): void; +} diff --git a/packages/opentelemetry-host-metrics/src/enum.ts b/packages/opentelemetry-host-metrics/src/enum.ts index 8b55597156..00ea94e9e4 100644 --- a/packages/opentelemetry-host-metrics/src/enum.ts +++ b/packages/opentelemetry-host-metrics/src/enum.ts @@ -15,24 +15,28 @@ */ export enum METRIC_NAMES { - CPU = 'cpu', - NETWORK = 'net', - MEMORY = 'mem', + CPU_TIME = 'system.cpu.time', + CPU_UTILIZATION = 'system.cpu.utilization', + MEMORY_USAGE = 'system.memory.usage', + MEMORY_UTILIZATION = 'system.memory.utilization', + NETWORK_DROPPED = 'system.network.dropped', + NETWORK_ERRORS = 'system.network.errors', + NETWORK_IO = 'system.network.io', } export enum CPU_LABELS { USER = 'user', - SYSTEM = 'sys', - USAGE = 'usage', - TOTAL = 'total', + SYSTEM = 'system', + IDLE = 'idle', } export enum NETWORK_LABELS { - BYTES_SENT = 'bytesSent', - BYTES_RECEIVED = 'bytesRecv', + DEVICE = 'device', + RECEIVE = 'receive', + TRANSMIT = 'transmit', } export enum MEMORY_LABELS { - AVAILABLE = 'available', - TOTAL = 'total', + FREE = 'free', + USED = 'used', } diff --git a/packages/opentelemetry-host-metrics/src/index.ts b/packages/opentelemetry-host-metrics/src/index.ts index e0a66d91fe..4554d5dbfa 100644 --- a/packages/opentelemetry-host-metrics/src/index.ts +++ b/packages/opentelemetry-host-metrics/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export * from './BaseMetrics'; export * from './metric'; export * from './types'; diff --git a/packages/opentelemetry-host-metrics/src/metric.ts b/packages/opentelemetry-host-metrics/src/metric.ts index ba6ae853ef..386408bef7 100644 --- a/packages/opentelemetry-host-metrics/src/metric.ts +++ b/packages/opentelemetry-host-metrics/src/metric.ts @@ -14,227 +14,208 @@ * limitations under the License. */ +import { BaseMetrics } from './BaseMetrics'; import * as api from '@opentelemetry/api'; -import * as metrics from '@opentelemetry/metrics'; import * as enums from './enum'; import { getCpuUsageData, getMemoryData } from './stats/common'; import { getNetworkData } from './stats/si'; - -import * as types from './types'; -import { VERSION } from './version'; - -/** - * Metrics Collector Configuration - */ -export interface MetricsCollectorConfig { - logger?: api.Logger; - // maximum timeout to wait for stats collection default is 500ms - maxTimeoutUpdateMS?: number; - // Meter Provider - meterProvider: metrics.MeterProvider; - // Character to be used to join metrics - default is "." - metricNameSeparator?: string; - // Name of component - name: string; - // metric export endpoint - url: string; -} - -const DEFAULT_MAX_TIMEOUT_UPDATE_MS = 500; -const DEFAULT_NAME = 'opentelemetry-host-metrics'; -const DEFAULT_METRIC_NAME_SEPARATOR = '.'; - -// default label name to be used to store metric name -const DEFAULT_KEY = 'name'; +import { CpuUsageData, MemoryData, NetworkData } from './types'; /** - * Metrics Collector - collects metrics for CPU, Memory, Heap, Network, Event - * Loop, Garbage Collector, Heap Space - * the default label name for metric name is "name" + * Metrics Collector - collects metrics for CPU, Memory, Network */ -export class HostMetrics { - protected _logger: api.Logger | undefined; - protected _maxTimeoutUpdateMS: number; - protected _meter: metrics.Meter; - private _name: string; - private _boundCounters: { [key: string]: api.BoundCounter } = {}; - private _metricNameSeparator: string; - - private _memValueObserver: types.ValueObserverWithObservations | undefined; - - constructor(config: MetricsCollectorConfig) { - this._logger = config.logger || new api.NoopLogger(); - this._name = config.name || DEFAULT_NAME; - this._maxTimeoutUpdateMS = - config.maxTimeoutUpdateMS || DEFAULT_MAX_TIMEOUT_UPDATE_MS; - this._metricNameSeparator = - config.metricNameSeparator || DEFAULT_METRIC_NAME_SEPARATOR; - const meterProvider = - config.meterProvider || api.metrics.getMeterProvider(); - if (!config.meterProvider) { - this._logger.warn('No meter provider, using default'); - } - this._meter = meterProvider.getMeter(this._name, VERSION); +export class HostMetrics extends BaseMetrics { + private _cpuTimeObserver!: api.SumObserver; + private _cpuUtilizationObserver!: api.ValueObserver; + private _memUsageObserver!: api.UpDownSumObserver; + private _memUtilizationObserver!: api.ValueObserver; + private _networkDroppedObserver!: api.SumObserver; + private _networkErrorsObserver!: api.SumObserver; + private _networkIOObserver!: api.SumObserver; + + private _updateCpuTime( + observerBatchResult: api.BatchObserverResult, + cpuUsage: CpuUsageData + ): void { + observerBatchResult.observe( + { + state: enums.CPU_LABELS.USER, + }, + [this._cpuTimeObserver?.observation(cpuUsage.user)] + ); + observerBatchResult.observe( + { + state: enums.CPU_LABELS.SYSTEM, + }, + [this._cpuTimeObserver?.observation(cpuUsage.system)] + ); + observerBatchResult.observe( + { + state: enums.CPU_LABELS.IDLE, + }, + [this._cpuTimeObserver?.observation(cpuUsage.idle)] + ); } - /** - * Creates a metric key name based on metric name and a key - * @param metricName - * @param key - */ - protected _boundKey(metricName: string, key: string) { - if (!key) { - return metricName; - } - return `${metricName}${this._metricNameSeparator}${key}`; + private _updateCpuUtilisation( + observerBatchResult: api.BatchObserverResult, + cpuUsage: CpuUsageData + ): void { + observerBatchResult.observe( + { + state: enums.CPU_LABELS.USER, + }, + [this._cpuUtilizationObserver?.observation(cpuUsage.userP)] + ); + observerBatchResult.observe( + { + state: enums.CPU_LABELS.SYSTEM, + }, + [this._cpuUtilizationObserver?.observation(cpuUsage.systemP)] + ); + observerBatchResult.observe( + { + state: enums.CPU_LABELS.IDLE, + }, + [this._cpuUtilizationObserver?.observation(cpuUsage.idleP)] + ); } - /** - * Updates counter based on boundkey - * @param metricName - * @param key - * @param value - */ - protected _counterUpdate(metricName: string, key: string, value = 0) { - const boundKey = this._boundKey(metricName, key); - this._boundCounters[boundKey].add(value); + private _updateMemUsage( + observerBatchResult: api.BatchObserverResult, + memUsage: MemoryData + ): void { + observerBatchResult.observe( + { + state: enums.MEMORY_LABELS.USED, + }, + [this._memUsageObserver?.observation(memUsage.used)] + ); + observerBatchResult.observe( + { + state: enums.MEMORY_LABELS.FREE, + }, + [this._memUsageObserver?.observation(memUsage.free)] + ); } - /** - * @param metricName metric name - this will be added as label under name - * "name" - * @param values values to be used to generate bound counters for each - * value prefixed with metricName - * @param description metric description - */ - protected _createCounter( - metricName: string, - values: string[], - description?: string - ) { - const keys = values.map(key => this._boundKey(metricName, key)); - const counter = this._meter.createCounter(metricName, { - description: description || metricName, - }); - keys.forEach(key => { - this._boundCounters[key] = counter.bind({ [DEFAULT_KEY]: key }); - }); + private _updateMemUtilization( + observerBatchResult: api.BatchObserverResult, + memUsage: MemoryData + ): void { + observerBatchResult.observe( + { + state: enums.MEMORY_LABELS.USED, + }, + [this._memUtilizationObserver?.observation(memUsage.usedP)] + ); + observerBatchResult.observe( + { + state: enums.MEMORY_LABELS.FREE, + }, + [this._memUtilizationObserver?.observation(memUsage.freeP)] + ); } - /** - * @param metricName metric name - this will be added as label under name - * "name" - * @param values values to be used to generate full metric name - * (metricName + value) - * value prefixed with metricName - * @param description metric description - * @param labelKey extra label to be observed - * @param labelValues label values to be observed based on labelKey - * @param afterKey extra name to be added to full metric name - */ - protected _createValueObserver( - metricName: string, - values: string[], - description: string, - labelKey = '', - labelValues: string[] = [], - afterKey = '' - ): types.ValueObserverWithObservations { - const labelKeys = [DEFAULT_KEY]; - if (labelKey) { - labelKeys.push(labelKey); - } - const observer = this._meter.createValueObserver(metricName, { - description: description || metricName, - }); - - const observations: types.Observations[] = []; - values.forEach(value => { - const boundKey = this._boundKey( - this._boundKey(metricName, value), - afterKey + private _updateNetwork( + observerBatchResult: api.BatchObserverResult, + networkUsages: NetworkData[] + ): void { + for (let i = 0, j = networkUsages.length; i < j; i++) { + const networkUsage = networkUsages[i]; + observerBatchResult.observe( + { + [enums.NETWORK_LABELS.DEVICE]: networkUsage.iface, + direction: enums.NETWORK_LABELS.RECEIVE, + }, + [this._networkDroppedObserver?.observation(networkUsage.rx_dropped)] + ); + observerBatchResult.observe( + { + device: networkUsage.iface, + direction: enums.NETWORK_LABELS.TRANSMIT, + }, + [this._networkDroppedObserver?.observation(networkUsage.tx_dropped)] ); - if (labelKey) { - // there is extra label to be observed mixed with default one - // for example we want to be able to observe "name" and "gc_type" - labelValues.forEach(label => { - const observedLabels = Object.assign( - {}, - { [DEFAULT_KEY]: boundKey }, - { - [labelKey]: label, - } - ); - observations.push({ - key: value, - labels: observedLabels, - labelKey, - }); - }); - } else { - observations.push({ - key: value, - labels: { [DEFAULT_KEY]: boundKey }, - }); - } - }); - - return { observer, observations }; - } - // MEMORY - private _createMemValueObserver() { - this._memValueObserver = this._createValueObserver( - enums.METRIC_NAMES.MEMORY, - Object.values(enums.MEMORY_LABELS), - 'Memory' - ); - } + observerBatchResult.observe( + { + device: networkUsage.iface, + direction: enums.NETWORK_LABELS.RECEIVE, + }, + [this._networkErrorsObserver?.observation(networkUsage.rx_errors)] + ); + observerBatchResult.observe( + { + device: networkUsage.iface, + direction: enums.NETWORK_LABELS.TRANSMIT, + }, + [this._networkErrorsObserver?.observation(networkUsage.tx_errors)] + ); - /** - * Updates observer - * @param observerBatchResult - * @param data - * @param observerWithObservations - */ - protected _updateObserver( - observerBatchResult: api.BatchObserverResult, - data: DataType, - observerWithObservations?: types.ValueObserverWithObservations - ) { - if (observerWithObservations) { - observerWithObservations.observations.forEach(observation => { - const value = data[observation.key as keyof DataType]; - if (typeof value === 'number') { - observerBatchResult.observe(observation.labels, [ - observerWithObservations.observer.observation(value), - ]); - } - }); + observerBatchResult.observe( + { + device: networkUsage.iface, + direction: enums.NETWORK_LABELS.RECEIVE, + }, + [this._networkIOObserver?.observation(networkUsage.rx_bytes)] + ); + observerBatchResult.observe( + { + device: networkUsage.iface, + direction: enums.NETWORK_LABELS.TRANSMIT, + }, + [this._networkIOObserver?.observation(networkUsage.tx_bytes)] + ); } } /** * Creates metrics */ - protected _createMetrics() { - // CPU COUNTER - this._createCounter( - enums.METRIC_NAMES.CPU, - Object.values(enums.CPU_LABELS), - 'CPU Usage' + protected _createMetrics(): void { + // CPU + this._cpuTimeObserver = this._meter.createSumObserver( + enums.METRIC_NAMES.CPU_TIME, + { description: 'Cpu time in seconds', unit: 's' } ); - - // NETWORK COUNTER - this._createCounter( - enums.METRIC_NAMES.NETWORK, - Object.values(enums.NETWORK_LABELS), - 'Network Usage' + this._cpuUtilizationObserver = this._meter.createValueObserver( + enums.METRIC_NAMES.CPU_UTILIZATION, + { + description: 'Cpu usage time 0-1', + } + ); + this._memUsageObserver = this._meter.createUpDownSumObserver( + enums.METRIC_NAMES.MEMORY_USAGE, + { + description: 'Memory usage in bytes', + } + ); + this._memUtilizationObserver = this._meter.createValueObserver( + enums.METRIC_NAMES.MEMORY_UTILIZATION, + { + description: 'Memory usage 0-1', + } + ); + this._networkDroppedObserver = this._meter.createSumObserver( + enums.METRIC_NAMES.NETWORK_DROPPED, + { + description: 'Network dropped packets', + } + ); + this._networkErrorsObserver = this._meter.createSumObserver( + enums.METRIC_NAMES.NETWORK_ERRORS, + { + description: 'Network errors counter', + } + ); + this._networkIOObserver = this._meter.createSumObserver( + enums.METRIC_NAMES.NETWORK_IO, + { + description: 'Network transmit and received bytes', + } ); - - // MEMORY - this._createMemValueObserver(); this._meter.createBatchObserver( 'metric_batch_observer', @@ -244,26 +225,11 @@ export class HostMetrics { getCpuUsageData(), getNetworkData(), ]).then(([memoryData, cpuUsage, networkData]) => { - // CPU COUNTER - Object.values(enums.CPU_LABELS).forEach(value => { - this._counterUpdate(enums.METRIC_NAMES.CPU, value, cpuUsage[value]); - }); - - // NETWORK COUNTER - Object.values(enums.NETWORK_LABELS).forEach(value => { - this._counterUpdate( - enums.METRIC_NAMES.NETWORK, - value, - networkData[value] - ); - }); - - // MEMORY - this._updateObserver( - observerBatchResult, - memoryData, - this._memValueObserver - ); + this._updateCpuTime(observerBatchResult, cpuUsage); + this._updateCpuUtilisation(observerBatchResult, cpuUsage); + this._updateMemUsage(observerBatchResult, memoryData); + this._updateMemUtilization(observerBatchResult, memoryData); + this._updateNetwork(observerBatchResult, networkData); }); }, { @@ -274,7 +240,7 @@ export class HostMetrics { } /** - * Starts collecting stats + * Starts collecting metrics */ start() { // initial collection diff --git a/packages/opentelemetry-host-metrics/src/stats/common.ts b/packages/opentelemetry-host-metrics/src/stats/common.ts index d3c25d1c4a..418f3fca28 100644 --- a/packages/opentelemetry-host-metrics/src/stats/common.ts +++ b/packages/opentelemetry-host-metrics/src/stats/common.ts @@ -15,33 +15,58 @@ */ import * as os from 'os'; -import { CPU_LABELS, MEMORY_LABELS } from '../enum'; import { CpuUsageData, MemoryData } from '../types'; const MICROSECOND = 1 / 1e6; -let cpuUsage: NodeJS.CpuUsage | undefined; +let cpuUsageTime: number | undefined = undefined; /** - * It returns cpu load delta from last time + * It returns cpu load delta from last time - to be used with SumObservers. + * When called first time it will return 0 and then delta will be calculated */ export function getCpuUsageData(): CpuUsageData { - const elapsedUsage = process.cpuUsage(cpuUsage); - cpuUsage = process.cpuUsage(); + if (typeof cpuUsageTime !== 'number') { + cpuUsageTime = new Date().getTime() - process.uptime() * 1000; + } + + const timeElapsed = (new Date().getTime() - cpuUsageTime) / 1000; + const elapsedUsage = process.cpuUsage(); + + const user = elapsedUsage.user * MICROSECOND; + const system = elapsedUsage.system * MICROSECOND; + const idle = Math.max(0, timeElapsed - user - system); + + const userP = user / timeElapsed; + const systemP = system / timeElapsed; + const idleP = idle / timeElapsed; + return { - [CPU_LABELS.USER]: elapsedUsage.user * MICROSECOND, - [CPU_LABELS.SYSTEM]: elapsedUsage.system * MICROSECOND, - [CPU_LABELS.USAGE]: (elapsedUsage.user + elapsedUsage.system) * MICROSECOND, - [CPU_LABELS.TOTAL]: (cpuUsage.user + cpuUsage.system) * MICROSECOND, + user: user, + system: system, + idle: idle, + userP: userP, + systemP: systemP, + idleP: idleP, }; } /** - * Returns memory data stats + * Returns memory data as absolute values */ export function getMemoryData(): MemoryData { + const total = os.totalmem(); + const free = os.freemem(); + + const used = total - free; + + const freeP = free / total; + const usedP = used / total; + return { - [MEMORY_LABELS.AVAILABLE]: os.freemem(), - [MEMORY_LABELS.TOTAL]: os.totalmem(), + used: used, + free: free, + usedP: usedP, // this is frac part (0-1) + freeP: freeP, // this is frac part (0-1) }; } diff --git a/packages/opentelemetry-host-metrics/src/stats/si.ts b/packages/opentelemetry-host-metrics/src/stats/si.ts index 4fb8c1f292..f31543e694 100644 --- a/packages/opentelemetry-host-metrics/src/stats/si.ts +++ b/packages/opentelemetry-host-metrics/src/stats/si.ts @@ -16,36 +16,13 @@ import * as SI from 'systeminformation'; import { NetworkData } from '../types'; -import { ObjectKeys } from '../util'; -let previousNetworkStats: Partial = {}; - -/** - * It returns network usage delta from last time - */ export function getNetworkData() { - return new Promise(resolve => { - const stats: NetworkData = { - bytesRecv: 0, - bytesSent: 0, - }; + return new Promise(resolve => { SI.networkStats() - .then(results => { - results.forEach(result => { - stats.bytesRecv += result.rx_bytes; - stats.bytesSent += result.tx_bytes; - }); - const lastStats = Object.assign({}, stats); - - ObjectKeys(stats).forEach(key => { - stats[key] = stats[key] - (previousNetworkStats[key] || 0); - }); - - previousNetworkStats = lastStats; - resolve(stats); - }) + .then(resolve) .catch(() => { - resolve(stats); + resolve([]); }); }); } diff --git a/packages/opentelemetry-host-metrics/src/types.ts b/packages/opentelemetry-host-metrics/src/types.ts index b47f4ed4a4..78fa9f07db 100644 --- a/packages/opentelemetry-host-metrics/src/types.ts +++ b/packages/opentelemetry-host-metrics/src/types.ts @@ -13,43 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { ValueObserver } from '@opentelemetry/api'; /** * Network data */ export interface NetworkData { - // bytes received - bytesRecv: number; - // bytes sent - bytesSent: number; + iface: string; + rx_bytes: number; + rx_dropped: number; + rx_errors: number; + tx_bytes: number; + tx_dropped: number; + tx_errors: number; } /** * CPU usage data */ export interface CpuUsageData { - sys: number; - usage: number; + system: number; user: number; - total: number; + idle: number; + systemP: number; + userP: number; + idleP: number; } /** * Memory data */ export interface MemoryData { - available: number; - total: number; -} - -export interface Observations { - key: string; - labels: Record; - labelKey?: string; -} - -export interface ValueObserverWithObservations { - observer: ValueObserver; - observations: Observations[]; + used: number; + free: number; + // cached: number; + usedP: number; + freeP: number; + // cachedP: number; } diff --git a/packages/opentelemetry-host-metrics/test/metric.test.ts b/packages/opentelemetry-host-metrics/test/metric.test.ts index b152a31a29..4a59ba179d 100644 --- a/packages/opentelemetry-host-metrics/test/metric.test.ts +++ b/packages/opentelemetry-host-metrics/test/metric.test.ts @@ -25,10 +25,10 @@ import { import * as assert from 'assert'; import * as os from 'os'; import * as sinon from 'sinon'; +import { HostMetrics } from '../src'; const cpuJson = require('./mocks/cpu.json'); const networkJson = require('./mocks/network.json'); -const memoryJson = require('./mocks/memory.json'); class NoopExporter implements MetricExporter { export( @@ -63,10 +63,11 @@ const mockedSI = { }); }, }; - +let memoryCallCount = 0; const mockedOS = { freemem: function () { - return 7179869184; + memoryCallCount++; + return 7179869184 + 1024 * memoryCallCount; }, totalmem: function () { return 17179869184; @@ -75,8 +76,6 @@ const mockedOS = { const INTERVAL = 3000; -let metrics: any; - describe('Host Metrics', () => { let sandbox: sinon.SinonSandbox; let hostMetrics: any; @@ -87,13 +86,15 @@ describe('Host Metrics', () => { sandbox = sinon.createSandbox(); sandbox.useFakeTimers(); - sandbox.stub(os, 'freemem').returns(mockedOS.freemem()); + sandbox.stub(os, 'freemem').callsFake(() => { + return mockedOS.freemem(); + }); sandbox.stub(os, 'totalmem').returns(mockedOS.totalmem()); sandbox.stub(process, 'cpuUsage').returns(cpuJson); - sandbox.stub(process, 'memoryUsage').returns(memoryJson); - const spyNetworkStats = sandbox - .stub(SI, 'networkStats') - .returns(mockedSI.networkStats()); + sandbox.stub(process, 'uptime').returns(0); + sandbox.stub(SI, 'networkStats').callsFake(() => { + return mockedSI.networkStats(); + }); exporter = new NoopExporter(); exportSpy = sandbox.stub(exporter, 'export'); @@ -103,30 +104,22 @@ describe('Host Metrics', () => { exporter, }); - // it seems like this is the only way to be able to mock - // `node-gyp-build` before metrics are being loaded, if import them before - // the first pass on unit tests will not mock correctly - metrics = require('../src'); - hostMetrics = new metrics.HostMetrics({ + hostMetrics = new HostMetrics({ meterProvider, name: 'opentelemetry-host-metrics', }); hostMetrics.start(); - // because networkStats mock simulates the network with every call it - // returns the data that is bigger then previous, it needs to stub it again - // as network is also called in initial start to start counting from 0 - spyNetworkStats.restore(); - sandbox.stub(SI, 'networkStats').returns(mockedSI.networkStats()); + countSI = 0; // sinon fake doesn't work fine with setImmediate originalSetTimeout(() => { // move the clock with the same value as interval - sandbox.clock.tick(INTERVAL); + sandbox.clock.tick(INTERVAL * 2); // move to "real" next tick so that async batcher observer will start // processing metrics originalSetTimeout(() => { - // allow all calbacks to finish correctly as they are finishing in + // allow all callbacks to finish correctly as they are finishing in // next tick due to async sandbox.clock.tick(1); originalSetTimeout(() => { @@ -140,39 +133,79 @@ describe('Host Metrics', () => { }); it('should create a new instance', () => { - assert.ok(hostMetrics instanceof metrics.HostMetrics); + assert.ok(hostMetrics instanceof HostMetrics); }); it('should create a new instance with default meter provider', () => { - hostMetrics = new metrics.HostMetrics({ + const meterProvider = new MeterProvider({ + interval: INTERVAL, + exporter, + }); + + hostMetrics = new HostMetrics({ + meterProvider, name: 'opentelemetry-host-metrics', }); - hostMetrics.start(); - assert.ok(hostMetrics instanceof metrics.HostMetrics); + hostMetrics.start(true); + assert.ok(hostMetrics instanceof HostMetrics); + }); + + it('should export CPU time metrics', () => { + const records = getRecords(exportSpy.args[0][0], 'system.cpu.time'); + assert.strictEqual(records.length, 3); + ensureValue(records[0], { state: 'user' }, 1.899243); + ensureValue(records[1], { state: 'system' }, 0.258553); + ensureValue(records[2], { state: 'idle' }, 3.8422039999999997); }); - it('should export CPU metrics', () => { - const records = getRecords(exportSpy.args[0][0], 'cpu'); - assert.strictEqual(records.length, 4); - ensureValue(records[0], 'cpu.user', 1.899243); - ensureValue(records[1], 'cpu.sys', 0.258553); - ensureValue(records[2], 'cpu.usage', 2.157796); - ensureValue(records[3], 'cpu.total', 2.157796); + it('should export CPU utilization metrics', () => { + const records = getRecords(exportSpy.args[0][0], 'system.cpu.utilization'); + assert.strictEqual(records.length, 3); + ensureValue(records[0], { state: 'user' }, 0.3165405); + ensureValue(records[1], { state: 'system' }, 0.04309216666666666); + ensureValue(records[2], { state: 'idle' }, 0.6403673333333333); + }); + + it('should export Memory usage metrics', done => { + const records = getRecords(exportSpy.args[0][0], 'system.memory.usage'); + assert.strictEqual(records.length, 2); + ensureValue(records[0], { state: 'used' }, 9999983616); + ensureValue(records[1], { state: 'free' }, 7179885568); + done(); + }); + + it('should export Memory utilization metrics', done => { + const records = getRecords( + exportSpy.args[0][0], + 'system.memory.utilization' + ); + assert.strictEqual(records.length, 2); + ensureValue(records[0], { state: 'used' }, 0.5820754766464233); + ensureValue(records[1], { state: 'free' }, 0.41792452335357666); + done(); + }); + + it('should export Network io dropped', done => { + const records = getRecords(exportSpy.args[0][0], 'system.network.dropped'); + assert.strictEqual(records.length, 2); + ensureValue(records[0], { direction: 'receive', device: 'eth0' }, 2400); + ensureValue(records[1], { direction: 'transmit', device: 'eth0' }, 24); + done(); }); - it('should export Network metrics', done => { - const records = getRecords(exportSpy.args[0][0], 'net'); + it('should export Network io errors', done => { + const records = getRecords(exportSpy.args[0][0], 'system.network.errors'); assert.strictEqual(records.length, 2); - ensureValue(records[0], 'net.bytesSent', 14207163202); - ensureValue(records[1], 'net.bytesRecv', 60073930753); + ensureValue(records[0], { direction: 'receive', device: 'eth0' }, 6); + ensureValue(records[1], { direction: 'transmit', device: 'eth0' }, 30); done(); }); - it('should export Memory metrics', done => { - const records = getRecords(exportSpy.args[0][0], 'mem'); + it('should export Network io bytes', done => { + const records = getRecords(exportSpy.args[0][0], 'system.network.io'); assert.strictEqual(records.length, 2); - ensureValue(records[0], 'mem.available', mockedOS.freemem()); - ensureValue(records[1], 'mem.total', mockedOS.totalmem()); + ensureValue(records[0], { direction: 'receive', device: 'eth0' }, 246246); + ensureValue(records[1], { direction: 'transmit', device: 'eth0' }, 642642); done(); }); }); @@ -181,8 +214,12 @@ function getRecords(records: MetricRecord[], name: string): MetricRecord[] { return records.filter(record => record.descriptor.name === name); } -function ensureValue(record: MetricRecord, name: string, value: number) { - assert.strictEqual(record.labels.name, name); +function ensureValue( + record: MetricRecord, + labels: Record, + value: number +) { + assert.deepStrictEqual(record.labels, labels); const point = record.aggregator.toPoint(); const aggValue = typeof point.value === 'number' diff --git a/packages/opentelemetry-host-metrics/test/mocks/memory.json b/packages/opentelemetry-host-metrics/test/mocks/memory.json deleted file mode 100644 index 2a565068ca..0000000000 --- a/packages/opentelemetry-host-metrics/test/mocks/memory.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rss": 152535045, - "heapTotal": 82182145, - "heapUsed": 53736445, - "external": 1543181 -} \ No newline at end of file diff --git a/packages/opentelemetry-host-metrics/test/mocks/network.json b/packages/opentelemetry-host-metrics/test/mocks/network.json index 77caed7d72..c14ffe084d 100644 --- a/packages/opentelemetry-host-metrics/test/mocks/network.json +++ b/packages/opentelemetry-host-metrics/test/mocks/network.json @@ -1,15 +1,14 @@ [ { - "iface": "en0", - "operstate": "up", - "rx_bytes": 60073930753, + "iface": "eth0", + "rx_bytes": 123123, "rx_dropped": 1200, - "rx_errors": 0, - "tx_bytes": 14207163202, - "tx_dropped": 1200, - "tx_errors": 21104, + "rx_errors": 3, + "tx_bytes": 321321, + "tx_dropped": 12, + "tx_errors": 15, "rx_sec": -1, "tx_sec": -1, "ms": 0 } -] \ No newline at end of file +] diff --git a/plugins/node/opentelemetry-koa-instrumentation/package.json b/plugins/node/opentelemetry-koa-instrumentation/package.json index 8da929046e..c5a6a60da4 100644 --- a/plugins/node/opentelemetry-koa-instrumentation/package.json +++ b/plugins/node/opentelemetry-koa-instrumentation/package.json @@ -15,7 +15,8 @@ "precompile": "tsc --version", "version:update": "node ../../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/plugins/node/opentelemetry-plugin-express/package.json b/plugins/node/opentelemetry-plugin-express/package.json index f565359ccc..515c376e1c 100644 --- a/plugins/node/opentelemetry-plugin-express/package.json +++ b/plugins/node/opentelemetry-plugin-express/package.json @@ -15,7 +15,8 @@ "precompile": "tsc --version", "version:update": "node ../../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/plugins/node/opentelemetry-plugin-pg-pool/package.json b/plugins/node/opentelemetry-plugin-pg-pool/package.json index f63616f359..12f1a46499 100644 --- a/plugins/node/opentelemetry-plugin-pg-pool/package.json +++ b/plugins/node/opentelemetry-plugin-pg-pool/package.json @@ -17,7 +17,8 @@ "precompile": "tsc --version", "version:update": "node ../../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/plugins/node/opentelemetry-plugin-pg/package.json b/plugins/node/opentelemetry-plugin-pg/package.json index 3eceff3bbf..6887244405 100644 --- a/plugins/node/opentelemetry-plugin-pg/package.json +++ b/plugins/node/opentelemetry-plugin-pg/package.json @@ -17,7 +17,8 @@ "precompile": "tsc --version", "version:update": "node ../../../scripts/version-update.js", "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "prepare": "npm run compile", + "watch": "tsc -w" }, "keywords": [ "opentelemetry",