diff --git a/src/metrics/network.ts b/src/metrics/network.ts new file mode 100644 index 00000000..f30e2607 --- /dev/null +++ b/src/metrics/network.ts @@ -0,0 +1,163 @@ +import * as netModule from 'net' +import MetricsFeature from '../features/metrics' +import MetricsInterface from './metricsInterface' +import MetricConfig from '../utils/metricConfig' + +import debug from 'debug' +debug('axm:network') + +export default class NetworkMetric implements MetricsInterface { + private metricFeature: MetricsFeature + + private defaultConf = { + ports: false, + traffic: true + } + + constructor (metricFeature: MetricsFeature) { + this.metricFeature = metricFeature + } + + init (config?) { + config = MetricConfig.getConfig(config, this.defaultConf) + + if (config.traffic) { + this.catchTraffic(config.traffic) + } + + if (config.ports) { + this.catchPorts() + } + } + + destroy () { + debug('NetworkMetric destroyed !') + } + + catchPorts () { + const portsList: Array = [] + let openedPorts = 'N/A' + + this.metricFeature.metric({ + name : 'Open ports', + value : function () { return openedPorts } + }) + + const originalListen = netModule.Server.prototype.listen + + netModule.Server.prototype.listen = function () { + const port = parseInt(arguments[0], 10) + + if (!isNaN(port) && portsList.indexOf(port) === -1) { + portsList.push(port) + openedPorts = portsList.sort().join() + } + + this.once('close', function () { + if (portsList.indexOf(port) > -1) { + portsList.splice(portsList.indexOf(port), 1) + openedPorts = portsList.sort().join() + } + }) + + return originalListen.apply(this, arguments) + } + } + + catchTraffic (config) { + let download = 0 + let upload = 0 + let up = '0 B/sec' + let down = '0 B/sec' + + const filter = function (bytes) { + let toFixed = 0 + + if (bytes < 1024) { + toFixed = 6 + } else if (bytes < (1024 * 1024)) { + toFixed = 3 + } else if (bytes !== 0) { + toFixed = 2 + } + + bytes = (bytes / (1024 * 1024)).toFixed(toFixed) + + let cutZeros = 0 + + for (let i = (bytes.length - 1); i > 0; --i) { + if (bytes[i] === '.') { + ++cutZeros + break + } + if (bytes[i] !== '0') break + ++cutZeros + } + + if (cutZeros > 0) { + bytes = bytes.slice(0, -(cutZeros)) + } + + return (bytes + ' MB/s') + } + + const interval = setInterval(function () { + up = filter(upload) + down = filter(download) + upload = 0 + download = 0 + }, 999) + + interval.unref() + + if (config === true || config.download === true) { + this.metricFeature.metric({ + name: 'Network Download', + agg_type: 'sum', + value: function () { + return down + } + }) + } + + if (config === true || config.upload === true) { + this.metricFeature.metric({ + name: 'Network Upload', + agg_type: 'sum', + value: function () { + return up + } + }) + } + + if (config === true || config.upload === true) { + const originalWrite = netModule.Socket.prototype.write + + netModule.Socket.prototype.write = function (data) { + if (data.length) { + upload += data.length + } + return originalWrite.apply(this, arguments) + } + } + + if (config === true || config.download === true) { + const originalRead = netModule.Socket.prototype.read + + netModule.Socket.prototype.read = function () { + + if (!this.monitored) { + this.monitored = true + + this.on('data', function (data) { + if (data.length) { + download += data.length + } + }) + } + + return originalRead.apply(this, arguments) + } + } + } +} diff --git a/src/services/metrics.ts b/src/services/metrics.ts index 97646a72..c5c02f80 100644 --- a/src/services/metrics.ts +++ b/src/services/metrics.ts @@ -6,6 +6,7 @@ import EventLoopDelayMetric from '../metrics/eventLoopDelay' import MetricConfig from '../utils/metricConfig' import EventLoopHandlesRequestsMetric from '../metrics/eventLoopHandlesRequests' import Transaction from '../metrics/transaction' +import NetworkMetric from '../metrics/network' debug('axm:metricService') @@ -26,6 +27,7 @@ export default class MetricsService { this.services.set('eventLoopDelay', new EventLoopDelayMetric(metricsFeature)) this.services.set('eventLoopActive', new EventLoopHandlesRequestsMetric(metricsFeature)) this.services.set('transaction', new Transaction(metricsFeature)) + this.services.set('network', new NetworkMetric(metricsFeature)) } init (config?, force?) { diff --git a/test/fixtures/features/networkChild.ts b/test/fixtures/features/networkChild.ts new file mode 100644 index 00000000..04d57fdd --- /dev/null +++ b/test/fixtures/features/networkChild.ts @@ -0,0 +1,24 @@ +import Metric from '../../../src/features/metrics' + +const metric = new Metric() +metric.init({network: {ports: true}}, true) + +const httpModule = require('http') + +let timer + +const server = httpModule.createServer((req, res) => { + res.writeHead(200) + res.end('hey') +}).listen(3002, () => { + timer = setInterval(function () { + httpModule.get('http://localhost:' + server.address().port) + httpModule.get('http://localhost:' + server.address().port + '/toto') + }, 100) +}) + +process.on('SIGINT', function () { + clearInterval(timer) + server.close() + metric.destroy() +}) diff --git a/test/fixtures/features/networkWithoutDownloadChild.ts b/test/fixtures/features/networkWithoutDownloadChild.ts new file mode 100644 index 00000000..ce46dc96 --- /dev/null +++ b/test/fixtures/features/networkWithoutDownloadChild.ts @@ -0,0 +1,24 @@ +import Metric from '../../../src/features/metrics' + +const metric = new Metric() +metric.init({network: {traffic: {upload: true}}}, true) + +const httpModule = require('http') + +let timer + +const server = httpModule.createServer((req, res) => { + res.writeHead(200) + res.end('hey') +}).listen(3002, () => { + timer = setInterval(function () { + httpModule.get('http://localhost:' + server.address().port) + httpModule.get('http://localhost:' + server.address().port + '/toto') + }, 100) +}) + +process.on('SIGINT', function () { + clearInterval(timer) + server.close() + metric.destroy() +}) diff --git a/test/fixtures/features/tracingChild.ts b/test/fixtures/features/tracingChild.ts index 94055e7e..f6b9974e 100644 --- a/test/fixtures/features/tracingChild.ts +++ b/test/fixtures/features/tracingChild.ts @@ -10,13 +10,11 @@ const httpModule = require('http') // test http outbound let timer -app.get('/', function (req, res){ - console.log('home') +app.get('/', function (req, res) { res.send('home') }) -app.get('/toto', function (req, res){ - console.log('toto') +app.get('/toto', function (req, res) { res.send('toto') }) diff --git a/test/metrics/network.spec.ts b/test/metrics/network.spec.ts new file mode 100644 index 00000000..9068f92a --- /dev/null +++ b/test/metrics/network.spec.ts @@ -0,0 +1,60 @@ +import { expect, assert } from 'chai' +import 'mocha' + +import SpecUtils from '../fixtures/utils' +import { fork, exec } from 'child_process' + +describe('Network', function () { + this.timeout(5000) + + it('should send network data', (done) => { + const child = fork(SpecUtils.buildTestPath('fixtures/features/networkChild.js')) + + child.on('message', pck => { + + if (pck.type === 'axm:monitor' && pck.data['Network Download'].value !== '0 B/sec') { + + expect(pck.data.hasOwnProperty('Network Download')).to.equal(true) + expect(pck.data['Network Download'].agg_type).to.equal('sum') + expect(pck.data['Network Download'].historic).to.equal(true) + expect(pck.data['Network Download'].type).to.equal('Network Download') + + expect(pck.data.hasOwnProperty('Network Upload')).to.equal(true) + expect(pck.data['Network Upload'].agg_type).to.equal('sum') + expect(pck.data['Network Upload'].historic).to.equal(true) + expect(pck.data['Network Upload'].type).to.equal('Network Upload') + + expect(pck.data.hasOwnProperty('Open ports')).to.equal(true) + expect(pck.data['Open ports'].value).to.equal('3002') + expect(pck.data['Open ports'].agg_type).to.equal('avg') + expect(pck.data['Open ports'].historic).to.equal(true) + expect(pck.data['Open ports'].type).to.equal('Open ports') + + child.kill('SIGINT') + done() + } + }) + }) + + it('should only send upload data', (done) => { + const child = fork(SpecUtils.buildTestPath('fixtures/features/networkWithoutDownloadChild.js')) + + child.on('message', pck => { + + if (pck.type === 'axm:monitor' && pck.data['Network Upload'].value !== '0 B/sec') { + + expect(pck.data.hasOwnProperty('Network Download')).to.equal(false) + + expect(pck.data.hasOwnProperty('Network Upload')).to.equal(true) + expect(pck.data['Network Upload'].agg_type).to.equal('sum') + expect(pck.data['Network Upload'].historic).to.equal(true) + expect(pck.data['Network Upload'].type).to.equal('Network Upload') + + expect(pck.data.hasOwnProperty('Open ports')).to.equal(false) + + child.kill('SIGINT') + done() + } + }) + }) +})