├── OSSMETADATA ├── scripts ├── post-install.sh ├── install-lib.js └── install-lib-from-gh.sh ├── .gitignore ├── doc ├── images │ ├── regular_timer.png │ ├── duration_timer.png │ ├── request_latency.png │ ├── hist_bucket_timer.png │ ├── perc_bucket_timer.png │ └── sla_bucket_timer.png ├── dist-summary.md ├── percentiles.md ├── counter.md ├── buckets.md ├── gauge.md ├── conventions.md ├── timer.md └── nodejs-metrics.md ├── src ├── atlas.h ├── start_stop.h ├── utils.h ├── atlas.cc ├── utils.cc ├── functions.h ├── start_stop.cc └── functions.cc ├── .github └── workflows │ ├── pr.yml │ ├── snapshot.yml │ └── release.yml ├── LICENSE ├── tools ├── publish_paths.js └── coverageBadge.js ├── test ├── metrics.test.js ├── load.test.js ├── mocks.test.js ├── api.test.js └── metrics-test.txt ├── binding.gyp ├── CONTRIBUTING.md ├── package.json ├── .clang-format ├── .jscsrc ├── Makefile ├── .eslintrc ├── mocks.js ├── node-metrics.js ├── index.js ├── README.md └── CHANGES.md /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=maintenance 2 | -------------------------------------------------------------------------------- /scripts/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run configure && npm run build 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | npm-debug.log 4 | coverage/ 5 | logs/ 6 | package-lock.json 7 | nc/ 8 | -------------------------------------------------------------------------------- /doc/images/regular_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/regular_timer.png -------------------------------------------------------------------------------- /doc/images/duration_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/duration_timer.png -------------------------------------------------------------------------------- /doc/images/request_latency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/request_latency.png -------------------------------------------------------------------------------- /doc/images/hist_bucket_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/hist_bucket_timer.png -------------------------------------------------------------------------------- /doc/images/perc_bucket_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/perc_bucket_timer.png -------------------------------------------------------------------------------- /doc/images/sla_bucket_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/atlas-node-client/main/doc/images/sla_bucket_timer.png -------------------------------------------------------------------------------- /src/atlas.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | atlas::Client& atlas_client(); 6 | atlas::meter::Registry* atlas_registry(); 7 | -------------------------------------------------------------------------------- /src/start_stop.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // start background processes for atlas plugin 6 | NAN_METHOD(start); 7 | 8 | // stop atlas plugin 9 | NAN_METHOD(stop); 10 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | bool tagsFromObject(v8::Isolate* isolate, const v8::Local& object, 7 | atlas::meter::Tags* tags, std::string* err_msg); 8 | 9 | atlas::meter::IdPtr idFromValue( 10 | const Nan::FunctionCallbackInfo& info, int argc); 11 | 12 | extern bool dev_mode; 13 | -------------------------------------------------------------------------------- /scripts/install-lib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require('child_process').exec; 4 | const read = require('fs').readFileSync; 5 | const pkgJson = JSON.parse(read('package.json')); 6 | const cmd = 'sh ./scripts/install-lib-from-gh.sh ' + pkgJson.native_version; 7 | 8 | exec(cmd, (error, stdout, stderr) => { 9 | if (error) { 10 | throw error; 11 | } 12 | 13 | if (stderr) { 14 | process.stderr.write('\x1b[31m' + stderr + '\x1b[0m'); 15 | } 16 | 17 | if (stdout) { 18 | process.stdout.write(stdout); 19 | } 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | node-version: [10, 12, 14] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Node ${{ matrix.node-version }} 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Install Dependencies 19 | run: npm install 20 | - name: Make 21 | run: make 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Netflix Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | if: ${{ github.repository == 'Netflix-Skunkworks/atlas-node-client' }} 11 | runs-on: macos-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | node-version: [10, 12, 14] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Node ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install Dependencies 23 | run: npm install 24 | - name: Make 25 | run: make 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | - v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+ 8 | 9 | jobs: 10 | build: 11 | if: ${{ github.repository == 'Netflix-Skunkworks/atlas-node-client' }} 12 | runs-on: macos-latest 13 | strategy: 14 | matrix: 15 | node-version: [10, 12, 14] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Node ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - name: Install Dependencies 23 | run: npm install 24 | - name: Make 25 | run: make 26 | -------------------------------------------------------------------------------- /scripts/install-lib-from-gh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | if [ -r nc/root/include/atlas/atlas_client.h ]; then 5 | echo Already installed 6 | cp nc/root/lib/libatlas* build/Release 7 | exit 0 8 | fi 9 | 10 | if [ $# = 0 ] ; then 11 | echo "Usage: $0 " >&2 12 | exit 1 13 | fi 14 | NATIVE_CLIENT_VERSION=$1 15 | 16 | rm -rf nc 17 | mkdir -p nc 18 | cd nc 19 | git init 20 | git remote add origin https://github.com/Netflix-Skunkworks/atlas-native-client.git 21 | git fetch origin $NATIVE_CLIENT_VERSION 22 | git reset --hard FETCH_HEAD 23 | mkdir -p build root 24 | cd build 25 | cmake -DCMAKE_INSTALL_PREFIX=/ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo .. 26 | make -j8 27 | make install DESTDIR=../root 28 | rm -f ../root/lib/libatlas*a 29 | cp ../root/lib/libatlas* ../../build/Release 30 | -------------------------------------------------------------------------------- /doc/dist-summary.md: -------------------------------------------------------------------------------- 1 | # Distribution Summaries 2 | 3 | A distribution summary is used to track the distribution of events. It is 4 | similar to a timer, but more general in that the size does not have to be 5 | a period of time. For example, a distribution summary could be used to measure 6 | the payload sizes of requests hitting a server. 7 | 8 | It is recommended to always use base units when recording the data. So if 9 | measuring the payload size use bytes, not kilobytes or some other unit. 10 | 11 | To get started create an instance of a Distribution Summary: 12 | 13 | ```js 14 | function Server(atlas) { 15 | this.requestSize = atlas.distSummary('server.requestSize'); 16 | } 17 | ``` 18 | 19 | Then call `record` when an event occurs: 20 | 21 | ```js 22 | Server.prototype.handle = function (req) { 23 | this.requestSize.record(getReqSizeInBytes(req)); 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /tools/publish_paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-console, no-process-exit */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const pkgjsonPath = path.join(__dirname, '../package.json'); 8 | const action = process.argv.length === 3 && process.argv[2]; 9 | const pkgjson = JSON.parse(fs.readFileSync(pkgjsonPath).toString()); 10 | const nodeABI = process.versions.modules; 11 | const commonPath = 'atlas-native-client/v' + pkgjson.version + 12 | '/Release/atlas-v' + pkgjson.version + 13 | '-node-v' + nodeABI + '-' + 14 | process.platform + '-x64.tar.gz'; 15 | 16 | if (action === 'stage') { 17 | console.log(path.join('build/stage', commonPath)); 18 | } else if (action === 'url') { 19 | console.log('http://artifacts.netflix.com/' + commonPath); 20 | } else { 21 | console.log('Unknown action', action); 22 | process.exit(1); 23 | } 24 | -------------------------------------------------------------------------------- /doc/percentiles.md: -------------------------------------------------------------------------------- 1 | # Percentile Meters 2 | 3 | ## Percentile Timer and Distribution Summaries 4 | ```js 5 | 6 | const timer = atlas.percentileTimer('requestLatency'); 7 | const distSummary = atlas.percentileDistSummary('responseBytes'); 8 | const start = process.hrtime(); 9 | //... 10 | 11 | timer.record(process.hrtime(start)); 12 | distSummary.record(getResponseSize(response)); 13 | 14 | 15 | // read api: compute a local (approximate) percentile 16 | // timers report times in seconds 17 | const median = timer.percentile(50); 18 | const tp95 = timer.percentile(95); 19 | const tp99 = timer.percentile(99); 20 | 21 | const dMedian = distSummary.percentile(50); 22 | 23 | // you can graph the meters using the :percentile atlas stack language operator 24 | // For example: 25 | // name,requestLatency,:eq,(,25,50,90,),:percentiles 26 | // see: https://github.com/Netflix/atlas/wiki/math-percentiles 27 | ``` 28 | -------------------------------------------------------------------------------- /test/metrics.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const metrics = require('../node-metrics.js'); 4 | const chai = require('chai'); 5 | const assert = chai.assert; 6 | 7 | describe('metrics', () => { 8 | it('should measure event loop tick', (done) => { 9 | // on github actions test runners, this can take longer than 50ms 10 | var ms = 75; 11 | var threshold = 10e6; // value is within 10ms 12 | var fakeSelf = { 13 | eventLoopTime: { 14 | record: (time) => { 15 | assert(time[0] === 0, 'should be less than a second'); 16 | assert( 17 | time[1] + threshold > ms * 1e6 && 18 | time[1] - threshold < ms * 1e6, 19 | 'nanosecond should be within tolerable threshold (time=' + 20 | time[1] + ')'); 21 | done(); 22 | } 23 | } 24 | }; 25 | metrics._measureEventLoopTime(fakeSelf); 26 | // Introduce ms worth of event loop work between invocations of setImmediate 27 | setImmediate(() => { 28 | var start = Date.now(); 29 | 30 | while (Date.now() - start < ms) {} // eslint-disable-line no-empty 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /doc/counter.md: -------------------------------------------------------------------------------- 1 | # Counters 2 | 3 | A counter is used to measure the rate at which some event is occurring. 4 | Consider a simple stack, counters would be used to measure things like the 5 | rate at which items are being pushed and popped. 6 | 7 | Counters are created using the atlas registry which will be setup as part of 8 | application initialization. For example: 9 | 10 | ```js 11 | function Stack(atlas) { 12 | this.size = 0; 13 | //... 14 | this.pushCounter = atlas.counter('pushCounter'); 15 | this.popCounter = atlas.counter('popCounter'); 16 | } 17 | 18 | Stack.prototype.push = function (item) { 19 | //... 20 | this.pushCounter.increment(); 21 | }; 22 | 23 | Stack.prototype.pop = function () { 24 | //... 25 | this.popCounter.increment(); 26 | }; 27 | ``` 28 | 29 | Optionally an amount can be passed in when calling `increment`. This is useful 30 | when a collection of events happens together. 31 | 32 | ```js 33 | Stack.prototype.pushAll = function (items) { 34 | // push each item in items 35 | //... 36 | this.pushCounter.increment(items.length); 37 | }; 38 | ``` 39 | 40 | In addition to `increment` counters also provide a `count` method that returns the 41 | total number of events that have occurred since the counter was created. 42 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'atlas', 5 | 'dependencies': ['libatlas'], 6 | 'sources': [" Atlas Native Client for nodejs. 4 | 5 | ## Building 6 | 7 | To compile the extension for the first time, run 8 | 9 | ``` 10 | $ npm i 11 | $ npm run configure 12 | $ npm run build 13 | ``` 14 | 15 | All subsequent builds only need `npm run build` 16 | 17 | You can confirm everything built correctly by [running the test suite](#to-run-tests). 18 | 19 | ### Working With the Extension Locally 20 | 21 | After building: 22 | 23 | ```node 24 | $ node 25 | > var atlas = require('./') 26 | undefined 27 | > atlas.counter("foo.bar").increment(); 28 | undefined 29 | > atlas.counter("foo.bar").count(); 30 | 1 31 | ``` 32 | 33 | ### To run tests: 34 | 35 | ``` 36 | $ npm test 37 | ``` 38 | 39 | or to run test continuously 40 | 41 | ``` 42 | $ npm test -- watch 43 | ``` 44 | 45 | ## The Parts 46 | 47 | File | Contents 48 | -------------|---------------- 49 | `atlas.cc` | Represents the top level of the module. C++ constructs that are exposed to javascript are exported here 50 | `functions.cc` | top-level functions. 51 | `index.js` | The main entry point for the node dependency 52 | `binding.gyp` | Describes your node native extension to the build system (`node-gyp`). As you add source files to the project, you should also add them to the binding file. 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atlasclient", 3 | "version": "1.23.13", 4 | "native_version": "v4.0.3", 5 | "main": "index.js", 6 | "homepage": "https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client", 7 | "repository": { 8 | "type": "git", 9 | "url": "ssh://git@stash.corp.netflix.com:7999/cldmta/atlas-node-client.git" 10 | }, 11 | "license": "Apache-2.0", 12 | "scripts": { 13 | "configure": "node-gyp configure", 14 | "build": "node-gyp build", 15 | "prepush": "make prepush", 16 | "codestyle": "make codestyle", 17 | "codestyle-fix": "make codestyle-fix", 18 | "install": "node-pre-gyp install --fallback-to-build", 19 | "postinstall": "make post-install", 20 | "test": "mocha --exit" 21 | }, 22 | "bundleDependencies": [ 23 | "node-pre-gyp" 24 | ], 25 | "devDependencies": { 26 | "chai": "^4.2.0", 27 | "coveralls": "^3.1.0", 28 | "eslint": "^7.2.0", 29 | "istanbul": "^0.4.5", 30 | "jscs": "^3.0.7", 31 | "mocha": "^8.0.1", 32 | "node-gyp": "^3.6.0", 33 | "sinon": "^9.0.2" 34 | }, 35 | "binary": { 36 | "module_name": "atlas", 37 | "module_path": "./build/Release/", 38 | "remote_path": "atlas-native-client/v{version}/{configuration}/", 39 | "host": "https://atlasclient.us-east-1.iepprod.netflix.net.s3-us-east-1.amazonaws.com" 40 | }, 41 | "dependencies": { 42 | "bindings": "^1.5.0", 43 | "nan": "^2.14.1", 44 | "node-pre-gyp": "^0.15.0", 45 | "pkginfo": "^0.4.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tools/coverageBadge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var COVERAGE_LINE_ID = '[manual coverage]'; 9 | var START_CHAR = '/coverage-'; 10 | var END_CHAR = ')]()'; 11 | 12 | var report = require('./../coverage/coverage-summary.json'); 13 | var pct = Math.round(report.total.lines.pct); 14 | var color; 15 | 16 | if (pct > 80) { 17 | color = 'green'; 18 | } else if (pct > 60) { 19 | color = 'yellow'; 20 | } else { 21 | color = 'red'; 22 | } 23 | 24 | // read in the README 25 | var README_PATH = path.join(__dirname, '../README.md'); 26 | var originalReadmeStr = fs.readFileSync(README_PATH).toString(); 27 | // process it 28 | var out = processLines(originalReadmeStr); 29 | // now write it back out 30 | fs.writeFileSync(README_PATH, out); 31 | 32 | 33 | function processLines(readmeStr) { 34 | 35 | var lines = readmeStr.toString().split('\n'); 36 | var outLines = ''; 37 | 38 | lines.forEach(function(line) { 39 | if (line.indexOf(COVERAGE_LINE_ID) > -1) { 40 | var startIdx = line.indexOf(START_CHAR); 41 | var endIdx = line.indexOf(END_CHAR); 42 | 43 | var newLine = [ 44 | line.slice(0, startIdx + 1), 45 | 'coverage-' + pct + '%25-' + color + '.svg', 46 | line.slice(endIdx), 47 | '\n' 48 | ].join(''); 49 | 50 | outLines += newLine; 51 | } else { 52 | outLines += line + '\n'; 53 | } 54 | }); 55 | 56 | // chop off last newline 57 | return outLines.slice(0, -1); 58 | } 59 | -------------------------------------------------------------------------------- /test/load.test.js: -------------------------------------------------------------------------------- 1 | // this tests whether a problematic set of metrics causes a crash 2 | // the metrics-test.txt file includes counters and timers with the same 3 | // name 4 | 'use strict'; 5 | 6 | 7 | function processMetric(atlas, type, name, tags, value) { 8 | switch (type) { 9 | case 'TIMER': 10 | const seconds = 0.0; 11 | const nanos = value * 1e6; 12 | atlas.timer(name, tags).record(seconds, nanos); 13 | break; 14 | case 'GAUGE': 15 | atlas.gauge(name, tags).update(value); 16 | break; 17 | case 'COUNTER': 18 | atlas.dcounter(name, tags).increment(value); 19 | break; 20 | default: 21 | console.log('Unknown type: ' + type); 22 | const payloadErrors = atlas.counter('atlas.agent.errors', { 23 | err: 'payload' 24 | }); 25 | 26 | payloadErrors.increment(); 27 | return false; 28 | } 29 | atlas.counter('atlas.agent.processed').increment(); 30 | return true; 31 | } 32 | 33 | describe('atlas timers/counters', () => { 34 | it('should not crash', (done) => { 35 | const lineReader = require('readline').createInterface({ 36 | input: require('fs').createReadStream('test/metrics-test.txt') 37 | }); 38 | 39 | const atlas = require('../'); 40 | lineReader.on('line', function(line) { 41 | const entry = JSON.parse(line); 42 | console.log('Handling ', entry); 43 | processMetric(atlas, entry.type, entry.name, 44 | entry.tags || {}, entry.value); 45 | }); 46 | 47 | lineReader.on('close', function() { 48 | console.log('Done!'); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /doc/buckets.md: -------------------------------------------------------------------------------- 1 | # Bucket Meters: BucketCounter, BucketTimer, BucketDistSummary 2 | 3 | The bucket meters allow you to get an idea of distribution related to known SLA. 4 | 5 | As an example let's consider an application that has a graph endpoint that should 6 | gather data from multiple sources, aggregate the data, and then present the results 7 | to the user. We have an SLA that specifies that requests should take under 4 seconds. 8 | We create a bucketTimer to track these requests, and consider anything taking 9 | over 8 seconds as too slow: 10 | 11 | ```js 12 | function Demo(atlas) { 13 | this.graphTimer = atlas.bucketTimer('demo.graphTime', 14 | { function: 'latency', value: 8, unit: 's' }); 15 | // ... 16 | } 17 | 18 | Demo.prototype.graph = function (req) { 19 | const start = process.hrtime(); 20 | doIt(function (err) { 21 | if (!err) { 22 | this.graphTimer.record(process.hrtime(start)); 23 | } 24 | }); 25 | }; 26 | 27 | ``` 28 | 29 | ![Histogram](images/hist_bucket_timer.png) 30 | 31 | ![As Percentage](images/perc_bucket_timer.png) 32 | 33 | ![Sla](images/sla_bucket_timer.png) 34 | 35 | ## Pros 36 | * Track whether the distribution is approaching SLA 37 | * Easily visualize distribution with RPS 38 | * Relatively cheap 39 | 40 | ## Cons 41 | * Need to configure it up front 42 | * Change how users think about the problem 43 | 44 | ## Bucket Functions 45 | 46 | ### Age 47 | 48 | ```js 49 | { function: 'age', value: 1, unit: 's' } 50 | ``` 51 | 52 | ### AgeBiasOld 53 | 54 | ```js 55 | { function: 'ageBiasOld', value: 1, unit: 's' } 56 | ``` 57 | 58 | ### Latency 59 | 60 | ```js 61 | { function: 'latency', value: 1, unit: 's' } 62 | ``` 63 | 64 | ### LatencyBiasOld 65 | 66 | ```js 67 | { function: 'latencyBiasOld', value: 1, unit: 's' } 68 | ``` 69 | ### Bytes 70 | 71 | ```js 72 | { function: 'bytes', value: 1048576 } 73 | ``` 74 | 75 | ### Decimal 76 | 77 | ```js 78 | { function: 'decimal', value: 1000 } 79 | ``` 80 | -------------------------------------------------------------------------------- /doc/gauge.md: -------------------------------------------------------------------------------- 1 | # Gauges 2 | 3 | A gauge is a handle to get the current value. Typical examples for gauges 4 | would be the size of a queue or the amount of heap memory used by an 5 | application. 6 | Since gauges are sampled, there is no information about what might have 7 | occurred between samples. 8 | 9 | Consider monitoring the behavior of a queue of tasks. If the data is being 10 | collected once a minute, then a gauge for the size will show the size when 11 | it was sampled. The size may have been much higher or lower at some point 12 | during interval, but that is not known. 13 | 14 | ## Example 15 | 16 | Let's create a simple module that will monitor the memory utilization of the nodejs process. We'll keep track of the `rss`, `heapTotal`, `heapUsed`, and `external` values as reported by `process.memoryUsage`: 17 | 18 | ```js 19 | function NodeMetrics(atlas) { 20 | this.atlas = atlas; 21 | this.rss = atlas.gauge('nodejs.rss'); 22 | this.heapTotal = atlas.gauge('nodejs.heapTotal'); 23 | this.heapUsed = atlas.gauge('nodejs.heapUsed'); 24 | this.external = atlas.gauge('nodejs.external'); 25 | } 26 | 27 | module.exports.NodeMetrics = NodeMetrics; 28 | 29 | NodeMetrics.prototype.start = function(refreshFreq) { 30 | let refresh = refreshFreq ? refreshFreq : 30000; 31 | this.intervalId = setInterval(refreshValues, refresh, this); 32 | } 33 | 34 | function refreshValues(self) { 35 | const memUsage = process.memoryUsage(); 36 | self.rss.update(memUsage.rss); 37 | self.heapTotal.update(memUsage.heapTotal); 38 | self.heapUsed.update(memUsage.heapUsed); 39 | self.external.update(memUsage.external); 40 | } 41 | ``` 42 | 43 | Note that we update the value periodically. The last value set before the metrics are sent to atlas is the value that we will observe in our graphs. 44 | 45 | We can also query the values programmatically: 46 | 47 | ```js 48 | > atlas.gauge('nodejs.rss').value() 49 | 26742784 50 | > atlas.gauge('nodejs.rss').value() 51 | 27095040 52 | ``` 53 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: true 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: true 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: true 22 | BinPackParameters: true 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: None 36 | BreakBeforeBraces: Attach 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializersBeforeComma: false 39 | ColumnLimit: 80 40 | CommentPragmas: '^ IWYU pragma:' 41 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 42 | ConstructorInitializerIndentWidth: 4 43 | ContinuationIndentWidth: 4 44 | Cpp11BracedListStyle: true 45 | DerivePointerAlignment: false 46 | DisableFormat: false 47 | ExperimentalAutoDetectBinPacking: false 48 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 49 | IncludeCategories: 50 | - Regex: '^<.*\.h>' 51 | Priority: 1 52 | - Regex: '^<.*' 53 | Priority: 2 54 | - Regex: '.*' 55 | Priority: 3 56 | IndentCaseLabels: true 57 | IndentWidth: 2 58 | IndentWrappedFunctionNames: false 59 | KeepEmptyLinesAtTheStartOfBlocks: false 60 | MacroBlockBegin: '' 61 | MacroBlockEnd: '' 62 | MaxEmptyLinesToKeep: 1 63 | NamespaceIndentation: None 64 | ObjCBlockIndentWidth: 2 65 | ObjCSpaceAfterProperty: false 66 | ObjCSpaceBeforeProtocolList: false 67 | PenaltyBreakBeforeFirstCallParameter: 1 68 | PenaltyBreakComment: 300 69 | PenaltyBreakFirstLessLess: 120 70 | PenaltyBreakString: 1000 71 | PenaltyExcessCharacter: 1000000 72 | PenaltyReturnTypeOnItsOwnLine: 200 73 | PointerAlignment: Left 74 | ReflowComments: true 75 | SortIncludes: false 76 | SpaceAfterCStyleCast: false 77 | SpaceBeforeAssignmentOperators: true 78 | SpaceBeforeParens: ControlStatements 79 | SpaceInEmptyParentheses: false 80 | SpacesBeforeTrailingComments: 2 81 | SpacesInAngles: false 82 | SpacesInContainerLiterals: true 83 | SpacesInCStyleCastParentheses: false 84 | SpacesInParentheses: false 85 | SpacesInSquareBrackets: false 86 | Standard: Auto 87 | TabWidth: 8 88 | UseTab: Never 89 | ... 90 | 91 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | // setup 3 | "fileExtensions": [ ".js" ], 4 | "excludeFiles": [ "./node_modules/**" ], 5 | 6 | // general rules 7 | "safeContextKeyword": [ "self" ], 8 | "validateParameterSeparator": ", ", 9 | "validateQuoteMarks": { 10 | "escape": true, 11 | "mark": "'" 12 | }, 13 | "requireLineFeedAtFileEnd": true, 14 | 15 | // alignment rules 16 | "maximumLineLength": { 17 | "value": 80, 18 | "allowComments": true, 19 | "allowUrlComments": true, 20 | "allowRegex": true 21 | }, 22 | "validateIndentation": 2, 23 | 24 | // disallow rules 25 | "disallowImplicitTypeConversion": [ 26 | "numeric", 27 | "boolean", 28 | "binary", 29 | "string" 30 | ], 31 | "disallowMixedSpacesAndTabs": true, 32 | "disallowMultipleVarDecl": "exceptUndefined", 33 | "disallowNewlineBeforeBlockStatements": true, 34 | "disallowOperatorBeforeLineBreak": [ "." ], 35 | "disallowQuotedKeysInObjects": true, 36 | "disallowSpaceAfterPrefixUnaryOperators": true, 37 | "disallowSpaceBeforePostfixUnaryOperators": true, 38 | "disallowSpacesInFunction": { 39 | "beforeOpeningRoundBrace": true 40 | }, 41 | "disallowSpacesInCallExpression": true, 42 | "disallowTrailingComma": true, 43 | "disallowTrailingWhitespace": true, 44 | "disallowYodaConditions": true, 45 | 46 | // require rules 47 | "requireBlocksOnNewline": true, 48 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 49 | "requireCapitalizedConstructors": true, 50 | "requireCurlyBraces": [ 51 | "if", 52 | "else", 53 | "for", 54 | "while", 55 | "do", 56 | "try", 57 | "catch" 58 | ], 59 | "requireDotNotation": true, 60 | "requireLineBreakAfterVariableAssignment": true, 61 | "requirePaddingNewLinesAfterUseStrict": true, 62 | "requirePaddingNewLinesBeforeExport": true, 63 | "requirePaddingNewlinesBeforeKeywords": [ 64 | "do", 65 | "for", 66 | "if", 67 | "switch", 68 | "try", 69 | "while" 70 | ], 71 | "requireSemicolons": true, 72 | "requireSpaceAfterBinaryOperators": true, 73 | "requireSpaceAfterKeywords": [ 74 | "do", 75 | "for", 76 | "if", 77 | "else", 78 | "switch", 79 | "case", 80 | "try", 81 | "catch", 82 | "while", 83 | "return", 84 | "typeof", 85 | "delete", 86 | "new", 87 | "void" 88 | ], 89 | "requireSpaceBeforeBinaryOperators": true, 90 | "requireSpaceBeforeBlockStatements": true, 91 | "requireSpaceBeforeKeywords": [ 92 | "else", 93 | "while", 94 | "catch" 95 | ], 96 | "requireSpaceBetweenArguments": true, 97 | "requireSpacesInConditionalExpression": true, 98 | "requireSpacesInForStatement": true, 99 | "requireSpacesInFunction": { 100 | "beforeOpeningCurlyBrace": true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/mocks.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const atlas = require('../mocks'); 4 | const assert = require('chai').assert; 5 | 6 | describe('atlas mocks', () => { 7 | it('should mock counters', () => { 8 | let counter = atlas.counter('foo'); 9 | counter.increment(); 10 | assert(counter); 11 | }); 12 | it('should mock interval counters', () => { 13 | let counter = atlas.intervalCounter('ic'); 14 | counter.increment(); 15 | assert(counter); 16 | }); 17 | it('should mock timers', () => { 18 | let timer = atlas.timer('timer'); 19 | timer.record(1, 100); 20 | assert(timer); 21 | }); 22 | it('should mock gauges', () => { 23 | let g = atlas.gauge('gauge'); 24 | g.update(42.0); 25 | assert(g); 26 | }); 27 | it('should mock age gauges', () => { 28 | let g = atlas.age('age'); 29 | g.update((new Date).getTime()); 30 | assert(g); 31 | }); 32 | it('should mock max gauges', () => { 33 | let g = atlas.maxGauge('mg', { 34 | k: 'v1' 35 | }); 36 | g.update(1.0); 37 | g.update(0.0); 38 | g.update(2.0); 39 | assert(g); 40 | }); 41 | it('should mock distribution summaries', () => { 42 | let ds = atlas.distSummary('ds'); 43 | ds.record(42.0); 44 | assert(ds); 45 | }); 46 | it('should mock long task timers', () => { 47 | let ltt = atlas.longTaskTimer('ltt'); 48 | ltt.record(420.0); 49 | assert(ltt); 50 | }); 51 | it('should mock percentile distribution summaries', () => { 52 | let pds = atlas.percentileDistSummary('pds'); 53 | pds.record(1.0); 54 | assert(pds); 55 | }); 56 | it('should mock percentile timers', () => { 57 | let pt = atlas.percentileTimer('pt'); 58 | pt.record(420.0); 59 | assert(pt); 60 | }); 61 | it('should mock bucket counters', () => { 62 | let bc = atlas.bucketCounter('bc', { 63 | function: 'latency', 64 | value: 3, 65 | unit: 's' 66 | }); 67 | bc.record(4.5); 68 | assert(bc); 69 | }); 70 | it('should mock bucket distribution summaries', () => { 71 | let bds = atlas.bucketDistSummary('bds', { 72 | function: 'decimal', 73 | value: 1000 74 | }); 75 | bds.record(420.0); 76 | assert(bds); 77 | }); 78 | it('should mock bucket timers', () => { 79 | let bt = atlas.bucketTimer('bt', { 80 | function: 'latencyBiasSlow', 81 | value: 5, 82 | unit: 'ms' 83 | }); 84 | bt.record(0.002); 85 | assert(bt); 86 | }); 87 | it('should mock start/stop', () => { 88 | atlas.start(); 89 | atlas.stop(); 90 | }); 91 | it('should mock getDebugInfo/config/measurements', () => { 92 | atlas.getDebugInfo(); 93 | atlas.config(); 94 | atlas.measurements(); 95 | }); 96 | it('should mock push', () => { 97 | atlas.push([]); 98 | }); 99 | it('should mock scope', () => { 100 | const a = atlas.scope({country: 'us'}); 101 | a.counter('foo').increment(); 102 | a.timer('foo').record([0, 42000]); 103 | const aa = a.scope({device: 'us'}); 104 | aa.counter('errors').increment(); 105 | assert(aa); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /doc/conventions.md: -------------------------------------------------------------------------------- 1 | Quick summary: 2 | 3 | 1. Names 4 | * Describe the measurement being collected 5 | * Use camel case 6 | * Static 7 | * Succinct 8 | 2. Tags 9 | * Should be used for dimensional drill-down 10 | * Be careful about combinatorial explosion 11 | * Tag keys should be static 12 | * Use `id` to distinguish between instances 13 | 3. Use base units 14 | 15 | ## Names 16 | 17 | ### Describe the measurement 18 | 19 | ### Use camel case 20 | 21 | The main goal here is to promote consistency which makes it easier for users. The choice of 22 | style is somewhat arbitrary, camel case was chosen because: 23 | 24 | * Used by snmp 25 | * Used by java 26 | * It was the most common in use at Netflix when this guideline was added 27 | 28 | The exception to this rule is where there is an established common case. For example with 29 | Amazon regions it is preferred to use us-east-1 rather than usEast1 as it is the more common 30 | form. 31 | 32 | ### Static 33 | 34 | There shouldn't be any dynamic content that goes into a metric name. Metric names and 35 | associated tag keys are how users will interact with the data being produced. 36 | 37 | ### Succinct 38 | 39 | Long names should be avoided. 40 | 41 | ## Tags 42 | 43 | Historically tags have been used to play one of two roles: 44 | 45 | * *Dimensions*: dimensions are the primary use and it allows the data to be sliced and diced so 46 | it is possible to drill down into the data. 47 | * *Namespace*: similar to packages in Java in this mode it would be used to group related data. 48 | This type of usage is discouraged. 49 | 50 | As a general rule it should be possible to use the name as a pivot. This means that if 51 | just the name is selected, then the user can drill down using other dimensions and be 52 | able to reason about the value being shown. 53 | 54 | As a concrete example, suppose we have two metrics: 55 | 56 | 1. The number of threads currently in a thread pool. 57 | 2. The number of rows in a database table. 58 | 59 | ### Bad approach 60 | 61 | ```java 62 | Id poolSize = registry.createId("size") 63 | .withTag("class", "ThreadPool") 64 | .withTag("id", "server-requests"); 65 | 66 | Id poolSize = registry.createId("size") 67 | .withTag("class", "Database") 68 | .withTag("table", "users"); 69 | ``` 70 | 71 | In this approach, if I select the name, `size`, it will match both the version for 72 | ThreadPool and Database classes. So you would get a value that is the an aggregate of the number 73 | of threads and the number of items in a database. 74 | 75 | ### Recommended 76 | 77 | ```java 78 | Id poolSize = registry.createId("threadpool.size") 79 | .withTag("id", "server-requests"); 80 | 81 | Id poolSize = registry.createId("db.size") 82 | .withTag("table", "users"); 83 | ``` 84 | 85 | This variant provides enough context so that if just the name is selected the value can 86 | be reasoned about and is at least potentially meaningful. For example if I select 87 | `threadpool.size` I can see the total number of threads in all pools. Then I can group by or 88 | select an `id` to drill down further. 89 | 90 | 91 | ## Use base units 92 | 93 | Keep measurements in base units where possible. For example I would rather have all timers 94 | in seconds, disk sizes should be bytes, or network rates should be bytes/second. The reason 95 | is that for my uses this usually means the unit is obvious from the name. It also means the 96 | SI prefix shown on the graph images make more sense, e.g. 1k is 1 kilobyte not 1 kilo-megabyte. 97 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Directories 3 | # 4 | ROOT := $(shell pwd) 5 | NODE_MODULES := $(ROOT)/node_modules 6 | NODE_BIN := $(NODE_MODULES)/.bin 7 | TOOLS := $(ROOT)/tools 8 | TMP := $(ROOT)/tmp 9 | 10 | # Publish Paths 11 | STAGE := $(shell node ./tools/publish_paths.js stage) 12 | PUBLISH_URL := $(shell node ./tools/publish_paths.js url) 13 | 14 | 15 | # 16 | # Tools and binaries 17 | # 18 | ESLINT := $(NODE_BIN)/eslint 19 | JSCS := $(NODE_BIN)/jscs 20 | PREGYP := $(NODE_BIN)/node-pre-gyp 21 | MOCHA := $(NODE_BIN)/mocha 22 | _MOCHA := $(NODE_BIN)/_mocha 23 | ISTANBUL := $(NODE_BIN)/istanbul 24 | COVERALLS := $(NODE_BIN)/coveralls 25 | NPM := npm 26 | GIT := git 27 | COVERAGE_BADGE := $(TOOLS)/coverageBadge.js 28 | 29 | 30 | # 31 | # Directories 32 | # 33 | LIB_FILES := $(ROOT)/ 34 | TEST_FILES := $(ROOT)/test 35 | COVERAGE_FILES := $(ROOT)/coverage 36 | 37 | 38 | # 39 | # Files and globs 40 | # 41 | CHANGESMD := $(ROOT)/CHANGES.md 42 | GIT_HOOK_SRC = '../../tools/githooks/pre-push' 43 | GIT_HOOK_DEST = '.git/hooks/pre-push' 44 | SHRINKWRAP := $(ROOT)/npm-shrinkwrap.json 45 | LCOV := $(ROOT)/coverage/lcov.info 46 | TEST_ENTRY := $(shell find $(TEST_FILES) -name '*.test.js') 47 | 48 | ALL_FILES := $(shell find $(ROOT) \ 49 | -not \( -path '*node_modules*' -prune \) \ 50 | -not \( -path $(COVERAGE_FILES) -prune \) \ 51 | -not \( -path '*/\.*' -prune \) \ 52 | -type f -name '*.js') 53 | 54 | # 55 | # Targets 56 | # 57 | 58 | .PHONY: all 59 | all: clean node_modules lint codestyle post-install test 60 | 61 | SYSTEM := $(shell uname -s) 62 | .PHONY: post-install 63 | post-install: 64 | ifeq ($(SYSTEM), Darwin) 65 | @echo Current path: $(shell otool -L build/Release/atlas.node | awk '/libatlasclient/{print $$1}') 66 | install_name_tool -change $(shell otool -L build/Release/atlas.node | awk '/libatlasclient/{print $$1}') @loader_path/libatlasclient.dylib build/Release/atlas.node 67 | endif 68 | rm -rf build/Release/obj.target build/Release/.deps 69 | 70 | node_modules: package.json build/Release 71 | @$(NPM) install 72 | @touch $(NODE_MODULES) 73 | 74 | build/Release: 75 | @$(NPM) install 76 | 77 | .PHONY: githooks 78 | githooks: 79 | @ln -s $(GIT_HOOK_SRC) $(GIT_HOOK_DEST) 80 | 81 | 82 | .PHONY: lint 83 | lint: node_modules $(ALL_FILES) 84 | @$(ESLINT) $(ALL_FILES) 85 | 86 | 87 | .PHONY: codestyle 88 | codestyle: node_modules $(ALL_FILES) 89 | @$(JSCS) $(ALL_FILES) 90 | 91 | 92 | .PHONY: codestyle-fix 93 | codestyle-fix: node_modules $(ALL_FILES) 94 | @$(JSCS) $(ALL_FILES) --fix 95 | 96 | 97 | .PHONY: nsp 98 | nsp: node_modules $(ALL_FILES) 99 | @$(NPM) shrinkwrap --dev 100 | @($(NSP) check) | $(NSP_BADGE) 101 | @rm $(SHRINKWRAP) 102 | 103 | 104 | .PHONY: prepush 105 | prepush: node_modules lint codestyle coverage 106 | 107 | .PHONY: package 108 | package: node_modules test lint codestyle 109 | @$(PREGYP) package 110 | 111 | .PHONY: publish 112 | publish: package 113 | # TODO 114 | @echo Not implemented 115 | 116 | .PHONY: test 117 | test: node_modules $(ALL_FILES) 118 | @$(MOCHA) -R spec $(TEST_ENTRY) --full-trace --exit 119 | 120 | 121 | .PHONY: coverage 122 | coverage: node_modules $(ALL_FILES) 123 | @$(ISTANBUL) cover $(_MOCHA) --report json-summary --report html -- -R spec 124 | @$(COVERAGE_BADGE) 125 | 126 | 127 | .PHONY: report-coverage 128 | report-coverage: coverage 129 | @cat $(LCOV) | $(COVERALLS) 130 | 131 | 132 | .PHONY: clean-coverage 133 | clean-coverage: 134 | @rm -rf $(COVERAGE_FILES) 135 | 136 | 137 | .PHONY: clean 138 | clean: clean-coverage 139 | @rm -rf $(NODE_MODULES) nc 140 | 141 | 142 | # 143 | ## Debug -- print out a a variable via `make print-FOO` 144 | # 145 | print-% : ; @echo $* = $($*) 146 | -------------------------------------------------------------------------------- /src/atlas.cc: -------------------------------------------------------------------------------- 1 | #include "atlas.h" 2 | #include "start_stop.h" 3 | #include "functions.h" 4 | 5 | using Nan::GetFunction; 6 | using Nan::New; 7 | using Nan::Set; 8 | using v8::FunctionTemplate; 9 | 10 | // atlas.cc represents the top level of the module. 11 | // C++ constructs that are exposed to javascript are exported here 12 | 13 | atlas::Client& atlas_client() { 14 | static atlas::Client client; 15 | return client; 16 | } 17 | 18 | atlas::meter::Registry* atlas_registry() { 19 | return atlas_client().GetRegistry().get(); 20 | } 21 | 22 | NAN_MODULE_INIT(InitAll) { 23 | Set(target, New("start").ToLocalChecked(), 24 | GetFunction(New(start)).ToLocalChecked()); 25 | 26 | Set(target, New("stop").ToLocalChecked(), 27 | GetFunction(New(stop)).ToLocalChecked()); 28 | 29 | Set(target, New("counter").ToLocalChecked(), 30 | GetFunction(New(counter)).ToLocalChecked()); 31 | 32 | Set(target, New("dcounter").ToLocalChecked(), 33 | GetFunction(New(dcounter)).ToLocalChecked()); 34 | 35 | Set(target, New("intervalCounter").ToLocalChecked(), 36 | GetFunction(New(interval_counter)).ToLocalChecked()); 37 | 38 | Set(target, New("timer").ToLocalChecked(), 39 | GetFunction(New(timer)).ToLocalChecked()); 40 | 41 | Set(target, New("longTaskTimer").ToLocalChecked(), 42 | GetFunction(New(long_task_timer)).ToLocalChecked()); 43 | 44 | Set(target, New("gauge").ToLocalChecked(), 45 | GetFunction(New(gauge)).ToLocalChecked()); 46 | 47 | Set(target, New("maxGauge").ToLocalChecked(), 48 | GetFunction(New(max_gauge)).ToLocalChecked()); 49 | 50 | Set(target, New("distSummary").ToLocalChecked(), 51 | GetFunction(New(dist_summary)).ToLocalChecked()); 52 | 53 | Set(target, New("bucketCounter").ToLocalChecked(), 54 | GetFunction(New(bucket_counter)).ToLocalChecked()); 55 | 56 | Set(target, New("bucketDistSummary").ToLocalChecked(), 57 | GetFunction(New(bucket_dist_summary)).ToLocalChecked()); 58 | 59 | Set(target, New("bucketTimer").ToLocalChecked(), 60 | GetFunction(New(bucket_timer)).ToLocalChecked()); 61 | 62 | Set(target, New("percentileTimer").ToLocalChecked(), 63 | GetFunction(New(percentile_timer)).ToLocalChecked()); 64 | 65 | Set(target, New("percentileDistSummary").ToLocalChecked(), 66 | GetFunction(New(percentile_dist_summary)) 67 | .ToLocalChecked()); 68 | 69 | Set(target, New("measurements").ToLocalChecked(), 70 | GetFunction(New(measurements)).ToLocalChecked()); 71 | 72 | Set(target, New("config").ToLocalChecked(), 73 | GetFunction(New(config)).ToLocalChecked()); 74 | 75 | Set(target, New("push").ToLocalChecked(), 76 | GetFunction(New(push)).ToLocalChecked()); 77 | 78 | Set(target, New("setDevMode").ToLocalChecked(), 79 | GetFunction(New(set_dev_mode)).ToLocalChecked()); 80 | 81 | Set(target, New("validateNameAndTags").ToLocalChecked(), 82 | GetFunction(New(validate_name_tags)).ToLocalChecked()); 83 | 84 | JsCounter::Init(target); 85 | JsDCounter::Init(target); 86 | JsIntervalCounter::Init(target); 87 | JsTimer::Init(target); 88 | JsLongTaskTimer::Init(target); 89 | JsGauge::Init(target); 90 | JsMaxGauge::Init(target); 91 | JsDistSummary::Init(target); 92 | JsBucketCounter::Init(target); 93 | JsBucketDistSummary::Init(target); 94 | JsBucketTimer::Init(target); 95 | JsPercentileTimer::Init(target); 96 | JsPercentileDistSummary::Init(target); 97 | } 98 | 99 | NODE_MODULE(Atlas, InitAll) 100 | -------------------------------------------------------------------------------- /doc/timer.md: -------------------------------------------------------------------------------- 1 | # Timers 2 | 3 | A timer is used to measure how long some event is taking. Two types of timers 4 | are supported: 5 | 6 | * `Timer`: for frequent short duration events. 7 | * `LongTaskTimer`: for long running tasks. 8 | 9 | The long duration timer is setup so that you can track the time while an 10 | event being measured is still running. A regular timer just records the 11 | duration and has no information until the task is complete. 12 | 13 | As an example, consider a chart showing request latency to a typical web 14 | server. The expectation is many short requests so the timer will be getting 15 | updated many times per second. 16 | 17 | ![Request Latency](images/request_latency.png) 18 | 19 | Now consider a background process to refresh metadata from a data store. For 20 | example, Edda caches AWS resources such as instances, volumes, auto-scaling 21 | groups etc. Normally all data can be refreshed in a few minutes. If the AWS 22 | services are having problems it can take much longer. A long duration timer 23 | can be used to track the overall time for refreshing the metadata. 24 | 25 | The charts below show max latency for the refresh using a regular timer and 26 | a long task timer. Regular timer, note that the y-axis is using a logarithmic 27 | scale: 28 | 29 | ![Regular Timer](images/regular_timer.png) 30 | 31 | Long task timer: 32 | 33 | ![Long Task Timer](images/duration_timer.png) 34 | 35 | ## Timer 36 | 37 | To get started create an instance of a timer: 38 | 39 | ```js 40 | function Server(atlas) { 41 | this.requestLatency = atlas.timer('server.requestLatency'); 42 | } 43 | ``` 44 | 45 | Then measure how long a short task takes. You can use the `timeAsync` helper 46 | to do this. Simply invoke the callback when your task is done. 47 | 48 | ```js 49 | Server.prototype.handle = function (request) { 50 | this.timer.timeAsync( (done) => { 51 | this.handleImpl(request, (err, value) => { 52 | if (!err) { 53 | done(); 54 | } 55 | }); 56 | } 57 | }; 58 | ``` 59 | 60 | Additionally there's a `time` helper for synchronous measurements, or you can 61 | explicitly call the `record` method. It takes an array (or two arguments) 62 | similar to what's returned by `process.hrtime()`: seconds, nanoseconds 63 | 64 | ```js 65 | 66 | Server.prototype.example = function (request) { 67 | const start = process.hrtime(); 68 | ///... 69 | 70 | const elapsed = process.hrtime(start); 71 | this.timer.record(elapsed); 72 | }; 73 | ``` 74 | 75 | ## LongTaskTimer 76 | 77 | First create an instance: 78 | 79 | ```js 80 | function MetadataService(atlas) { 81 | this.metadataRefresh = atlas.longTaskTimer('metadata.refreshDuration'); 82 | // setup async process to call refresh() 83 | } 84 | 85 | MetadataService.prototype.refresh = function () { 86 | const id = this.metadataRefresh.start(); 87 | this.refreshImpl(() => { this.metadataRefresh.stop(id) }); 88 | }; 89 | ``` 90 | 91 | The `id` is used to keep track 92 | of a particular task being measured by the timer. 93 | It must be stopped using the provided id. Note that unlike a regular timer 94 | that does not do anything until the final duration is recorded, a long duration 95 | timer will report as two gauges: 96 | 97 | * `duration`: total duration spent within all currently running tasks. 98 | * `activeTasks`: number of currently running tasks. 99 | 100 | This means that you can see what is happening while the task is running, but 101 | you need to keep in mind: 102 | 103 | * The id is fixed before the task begins. There is no way to change tags based 104 | on the run, e.g., update a different timer if an exception is thrown. 105 | * Being a gauge it is inappropriate for short tasks. In particular, gauges are 106 | sampled and if it is not sampled during the execution or the sampling period 107 | is a significant subset of the expected duration, then the duration value 108 | will not be meaningful. 109 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "mocha": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "rules": { 9 | // possible errors 10 | "comma-dangle": [ 2 ], 11 | "no-cond-assign": [ 2 ], 12 | "no-console": [ 0 ], 13 | "no-constant-condition": [ 2 ], 14 | "no-control-regex": [ 2 ], 15 | "no-debugger": [ 2 ], 16 | "no-dupe-args": [ 2 ], 17 | "no-dupe-keys": [ 2 ], 18 | "no-duplicate-case": [ 2 ], 19 | "no-empty": [ 2 ], 20 | "no-empty-character-class": [ 2 ], 21 | "no-ex-assign": [ 2 ], 22 | "no-extra-boolean-cast": [ 2 ], 23 | "no-extra-semi": [ 2 ], 24 | "no-func-assign": [ 2 ], 25 | "no-invalid-regexp": [ 2 ], 26 | "no-irregular-whitespace": [ 2 ], 27 | "no-negated-in-lhs": [ 2 ], 28 | "no-reserved-keys": [ 0 ], 29 | "no-regex-spaces": [ 2 ], 30 | "no-sparse-arrays": [ 2 ], 31 | "no-unreachable": [ 2 ], 32 | "use-isnan": [ 2 ], 33 | "valid-typeof": [ 2 ], 34 | "valid-jsdoc": [ 2, { 35 | "requireReturnDescription": false 36 | }], 37 | 38 | // best practices 39 | "block-scoped-var": [ 2 ], 40 | "consistent-return": [ 2 ], 41 | "curly": [ 2 ], 42 | "default-case": [ 2 ], 43 | "dot-notation": [ 2, { "allowKeywords": true } ], 44 | "eqeqeq": [ 2 ], 45 | "guard-for-in": [ 0 ], 46 | "no-alert": [ 2 ], 47 | "no-caller": [ 2 ], 48 | "no-div-regex": [ 2 ], 49 | "no-eq-null": [ 2 ], 50 | "no-eval": [ 2 ], 51 | "no-extend-native": [ 2 ], 52 | "no-extra-bind": [ 2 ], 53 | "no-fallthrough": [ 2 ], 54 | "no-floating-decimal": [ 2 ], 55 | "no-implied-eval": [ 2 ], 56 | "no-iterator": [ 2 ], 57 | "no-labels": [ 2 ], 58 | "no-lone-blocks": [ 2 ], 59 | "no-loop-func": [ 2 ], 60 | "no-multi-spaces": [ 0 ], 61 | "no-native-reassign": [ 2 ], 62 | "no-new": [ 2 ], 63 | "no-new-func": [ 2 ], 64 | "no-new-wrappers": [ 2 ], 65 | "no-octal": [ 2 ], 66 | "no-octal-escape": [ 2 ], 67 | "no-param-reassign": [ 2 ], 68 | "no-proto": [ 2 ], 69 | "no-redeclare": [ 2 ], 70 | "no-return-assign": [ 2 ], 71 | "no-script-url": [ 2 ], 72 | "no-self-compare": [ 2 ], 73 | "no-sequences": [ 2 ], 74 | "no-throw-literal": [ 2 ], 75 | "no-unused-expressions": [ 2 ], 76 | "no-void": [ 2 ], 77 | "no-warning-comments": [ 1 ], 78 | "no-with": [ 2 ], 79 | "wrap-iife": [ 2 ], 80 | "yoda": [ 2, "never" ], 81 | 82 | // strict mode 83 | "strict": [ 2, "global" ], 84 | 85 | // variables 86 | "no-catch-shadow": [ 2 ], 87 | "no-delete-var": [ 2 ], 88 | "no-shadow": [ 2 ], 89 | "no-shadow-restricted-names": [ 2 ], 90 | "no-undef": [ 2 ], 91 | "no-undef-init": [ 2 ], 92 | "no-undefined": [ 0 ], 93 | "no-unused-vars": [ 2, { "vars": "all", "args": "none" } ], 94 | "no-use-before-define": [ 2, "nofunc" ], 95 | 96 | // node.js 97 | "handle-callback-err": [ 2, "^.*(e|E)rr" ], 98 | "no-mixed-requires": [ 2 ], 99 | "no-new-require": [ 2 ], 100 | "no-path-concat": [ 2 ], 101 | "no-process-exit": [ 0 ], 102 | 103 | // ES6 104 | "generator-star-spacing": [ 2, "after" ], 105 | 106 | // stylistic 107 | // we use JSCS, set most to off because they're on by default. 108 | // turn the few on that aren't handled by JSCS today. 109 | "camelcase": [ 0 ], 110 | "key-spacing": [ 0 ], 111 | "no-lonely-if": [ 0 ], 112 | "no-multi-str": [ 0 ], 113 | "no-underscore-dangle": [ 0 ], 114 | "quotes": [ 0 ], 115 | "semi": [ 0 ], 116 | "space-infix-ops": [ 0 ], 117 | "space-return-throw-case": [ 0 ], 118 | "space-unary-ops": [ 0 ], 119 | 120 | "no-array-constructor": [ 2 ], 121 | "no-nested-ternary": [ 2 ], 122 | "no-new-object": [ 2 ], 123 | "complexity": [ 2 ], 124 | "no-else-return": [ 2 ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /mocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | 5 | function atlas() { 6 | // mock atlas 7 | const counter = sinon.stub(); 8 | const counterIncrement = sinon.spy(); 9 | const intervalCounter = sinon.stub(); 10 | const intervalCounterIncrement = sinon.spy(); 11 | const timer = sinon.stub(); 12 | const timerRecord = sinon.spy(); 13 | const maxGauge = sinon.stub(); 14 | const maxGaugeUpdate = sinon.spy(); 15 | const gauge = sinon.stub(); 16 | const gaugeUpdate = sinon.spy(); 17 | const distSummary = sinon.stub(); 18 | const distRecord = sinon.spy(); 19 | const longTaskTimer = sinon.stub(); 20 | const longTaskTimerRecord = sinon.spy(); 21 | const percentileDistSummary = sinon.stub(); 22 | const percentileDistSummaryRecord = sinon.spy(); 23 | const percentileTimer = sinon.stub(); 24 | const percentileTimerRecord = sinon.spy(); 25 | const age = sinon.stub(); 26 | const ageUpdate = sinon.spy(); 27 | const bucketCounter = sinon.stub(); 28 | const bucketCounterRecord = sinon.spy(); 29 | const bucketDistSummary = sinon.stub(); 30 | const bucketDistSummaryRecord = sinon.spy(); 31 | const bucketTimer = sinon.stub(); 32 | const bucketTimerRecord = sinon.spy(); 33 | const getDebugInfo = sinon.spy(); 34 | const start = sinon.spy(); 35 | const stop = sinon.spy(); 36 | const config = sinon.spy(); 37 | const measurements = sinon.spy(); 38 | const push = sinon.spy(); 39 | const apiExceptScope = { 40 | counter: counter.returns({ 41 | increment: counterIncrement 42 | }), 43 | intervalCounter: intervalCounter.returns({ 44 | increment: intervalCounterIncrement 45 | }), 46 | timer: timer.returns({ 47 | record: timerRecord 48 | }), 49 | gauge: gauge.returns({ 50 | update: gaugeUpdate 51 | }), 52 | maxGauge: maxGauge.returns({ 53 | update: maxGaugeUpdate 54 | }), 55 | distSummary: distSummary.returns({ 56 | record: distRecord 57 | }), 58 | longTaskTimer: longTaskTimer.returns({ 59 | record: longTaskTimerRecord 60 | }), 61 | percentileDistSummary: percentileDistSummary.returns({ 62 | record: percentileDistSummaryRecord 63 | }), 64 | percentileTimer: percentileTimer.returns({ 65 | record: percentileTimerRecord 66 | }), 67 | age: age.returns({ 68 | update: ageUpdate 69 | }), 70 | bucketCounter: bucketCounter.returns({ 71 | record: bucketCounterRecord 72 | }), 73 | bucketDistSummary: bucketDistSummary.returns({ 74 | record: bucketDistSummaryRecord 75 | }), 76 | bucketTimer: bucketTimer.returns({ 77 | record: bucketTimerRecord 78 | }), 79 | getDebugInfo: getDebugInfo, 80 | start: start, 81 | stop: stop, 82 | config: config, 83 | measurements: measurements, 84 | push: push 85 | }; 86 | const scope = sinon.stub(); 87 | scope.returns(Object.assign({}, apiExceptScope, {scope: scope})); 88 | const stubs = { 89 | counter: counter, 90 | counterIncrement: counterIncrement, 91 | intervalCounter: intervalCounter, 92 | intervalCounterIncrement: intervalCounterIncrement, 93 | timer: timer, 94 | timerRecord: timerRecord, 95 | gauge: gauge, 96 | gaugeUpdate: gaugeUpdate, 97 | maxGauge: maxGauge, 98 | maxGaugeUpdate: maxGaugeUpdate, 99 | distSummary: distSummary, 100 | distSummaryRecord: distRecord, 101 | longTaskTimer: longTaskTimer, 102 | longTaskTimerRecord: longTaskTimerRecord, 103 | percentileDistSummary: percentileDistSummary, 104 | percentileDistSummaryRecord: percentileDistSummaryRecord, 105 | percentileTimer: percentileTimer, 106 | percentileTimerRecord: percentileTimerRecord, 107 | age: age, 108 | ageUpdate: ageUpdate, 109 | bucketCounter: bucketCounter, 110 | bucketCounterRecord: bucketCounterRecord, 111 | bucketDistSummary: bucketDistSummary, 112 | bucketDistSummaryRecord: bucketDistSummaryRecord, 113 | bucketTimer: bucketTimer, 114 | bucketTimerRecord: bucketTimerRecord, 115 | getDebugInfo: getDebugInfo, 116 | start: start, 117 | stop: stop, 118 | config: config, 119 | scope: scope 120 | }; 121 | 122 | const mocks = Object.assign({}, apiExceptScope, stubs); 123 | return mocks; 124 | } 125 | 126 | module.exports = atlas(); 127 | -------------------------------------------------------------------------------- /src/utils.cc: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include "atlas.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using atlas::meter::AnalyzeTags; 9 | using atlas::meter::IdPtr; 10 | using atlas::meter::Tags; 11 | using atlas::meter::ValidationIssue; 12 | using atlas::util::intern_str; 13 | using atlas::util::StartsWith; 14 | using atlas::util::StrRef; 15 | 16 | bool dev_mode = false; 17 | 18 | std::ostream& operator<<(std::ostream& os, const Tags& tags) { 19 | os << '{'; 20 | auto first = true; 21 | for (const auto& kv : tags) { 22 | if (first) { 23 | first = false; 24 | } else { 25 | os << ", "; 26 | } 27 | os << kv.first.get() << '=' << kv.second.get(); 28 | } 29 | os << '}'; 30 | return os; 31 | } 32 | 33 | static void throw_if_invalid(const std::string& name, const Tags& tags) { 34 | Tags to_check{tags}; 35 | to_check.add("name", name.c_str()); 36 | 37 | auto res = atlas::meter::AnalyzeTags(to_check); 38 | // no warnings or errors found 39 | if (res.empty()) { 40 | return; 41 | } 42 | 43 | auto errs = 0; 44 | std::ostringstream err_msg; 45 | err_msg << "[name:'" << name << "', tags:" << tags << "]:\n"; 46 | for (const auto& issue : res) { 47 | if (issue.level == ValidationIssue::Level::ERROR) { 48 | ++errs; 49 | } 50 | 51 | err_msg << '\t' << issue.ToString() << '\n'; 52 | } 53 | 54 | // do not throw if warnings only 55 | if (errs > 0) { 56 | auto err = err_msg.str(); 57 | Nan::ThrowError(err.c_str()); 58 | } 59 | } 60 | 61 | bool tagsFromObject(v8::Isolate* isolate, const v8::Local& object, 62 | Tags* tags, std::string* error_msg) { 63 | auto context = isolate->GetCurrentContext(); 64 | auto maybe_props = object->GetOwnPropertyNames(context); 65 | if (!maybe_props.IsEmpty()) { 66 | auto props = maybe_props.ToLocalChecked(); 67 | auto n = props->Length(); 68 | for (uint32_t i = 0; i < n; ++i) { 69 | const auto& key = props->Get(context, i).ToLocalChecked(); 70 | const auto& value = object->Get(context, key).ToLocalChecked(); 71 | const auto& k = std::string(*Nan::Utf8String(key)); 72 | const auto& v = std::string(*Nan::Utf8String(value)); 73 | if (k.empty()) { 74 | *error_msg = "Cannot have an empty key when specifying tags"; 75 | return false; 76 | } 77 | if (v.empty()) { 78 | *error_msg = "Cannot have an empty value for key '" + k + 79 | "' when specifying tags"; 80 | return false; 81 | } 82 | tags->add(k.c_str(), v.c_str()); 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | IdPtr idFromValue(const Nan::FunctionCallbackInfo& info, int argc) { 89 | std::string err_msg; 90 | Tags tags; 91 | std::string name; 92 | auto r = atlas_registry(); 93 | 94 | if (argc == 0) { 95 | err_msg = "Need at least one argument to specify a metric name"; 96 | goto error; 97 | } else if (argc > 2) { 98 | err_msg = 99 | "Expecting at most two arguments: a name and an object describing tags"; 100 | goto error; 101 | } 102 | name = *Nan::Utf8String(info[0]); 103 | if (name.empty()) { 104 | err_msg = "Cannot create a metric with an empty name"; 105 | goto error; 106 | } 107 | 108 | if (argc == 2) { 109 | // read the object which should just have string keys and string values 110 | const auto& maybe_o = info[1]; 111 | if (maybe_o->IsObject()) { 112 | auto context = Nan::GetCurrentContext(); 113 | if (!tagsFromObject(info.GetIsolate(), 114 | maybe_o->ToObject(context).ToLocalChecked(), &tags, 115 | &err_msg)) { 116 | err_msg += " for metric name '" + name + "'"; 117 | goto error; 118 | } 119 | } else { 120 | err_msg = 121 | "Expected an object with string keys and string values as the second " 122 | "argument"; 123 | goto error; 124 | } 125 | } 126 | 127 | // do expensive validation only in dev mode 128 | if (dev_mode) { 129 | throw_if_invalid(name, tags); 130 | } 131 | return r->CreateId(name, tags); 132 | error: 133 | if (dev_mode) { 134 | Nan::ThrowError(err_msg.c_str()); 135 | } else { 136 | fprintf(stderr, "Error creating atlas metric ID: %s\n", err_msg.c_str()); 137 | } 138 | tags.add("atlas.invalid", "true"); 139 | return r->CreateId("invalid", tags); 140 | } 141 | -------------------------------------------------------------------------------- /node-metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const v8 = require('v8'); 4 | 5 | function NodeMetrics(atlas, extraTags) { 6 | this.atlas = atlas; 7 | this.rss = atlas.gauge('nodejs.rss', extraTags); 8 | this.heapTotal = atlas.gauge('nodejs.heapTotal', extraTags); 9 | this.heapUsed = atlas.gauge('nodejs.heapUsed', extraTags); 10 | this.external = atlas.gauge('nodejs.external', extraTags); 11 | this.extraTags = extraTags; 12 | this.running = false; 13 | 14 | if (typeof process.cpuUsage === 'function') { 15 | this.cpuUsageUser = atlas.gauge('nodejs.cpuUsage', { 16 | id: 'user', 17 | 'nodejs.version': process.version 18 | }); 19 | this.cpuUsageSystem = atlas.gauge('nodejs.cpuUsage', { 20 | id: 'system', 21 | 'nodejs.version': process.version 22 | }); 23 | this.lastCpuUsage = process.cpuUsage(); 24 | this.lastCpuUsageTime = process.hrtime(); 25 | } 26 | } 27 | module.exports.NodeMetrics = NodeMetrics; 28 | 29 | function measureEventLoopTime(self) { 30 | setImmediate(function() { 31 | const start = process.hrtime(); 32 | setImmediate(function() { 33 | self.eventLoopTime.record(process.hrtime(start)); 34 | }); 35 | }); 36 | } 37 | 38 | module.exports._measureEventLoopTime = measureEventLoopTime; 39 | 40 | NodeMetrics.prototype.start = function(refreshFreq) { 41 | this.running = true; 42 | const refresh = refreshFreq ? refreshFreq : 30000; 43 | this.intervalId = setInterval(refreshValues, refresh, this); 44 | this.eventLoopTime = this.atlas.timer('nodejs.eventLoop', this.extraTags); 45 | this.eventLoopFreq = 500; 46 | this.eventLoopInterval = setInterval(measureEventLoopTime, 47 | this.eventLoopFreq, this); 48 | measureEventLoopTime(this); 49 | }; 50 | 51 | NodeMetrics.prototype.stop = function() { 52 | this.running = false; 53 | 54 | if (this.intervalId) { 55 | clearInterval(this.intervalId); 56 | this.intervalId = undefined; 57 | } 58 | 59 | if (this.eventLoopImmediate) { 60 | clearImmediate(this.eventLoopImmediate); 61 | this.eventLoopImmediate = undefined; 62 | } 63 | 64 | if (this.eventLoopInterval) { 65 | clearInterval(this.eventLoopInterval); 66 | this.eventLoopInterval = undefined; 67 | } 68 | }; 69 | 70 | function deltaMicros(end, start) { 71 | let deltaNanos = end[1] - start[1]; 72 | let deltaSecs = end[0] - start[0]; 73 | 74 | if (deltaNanos < 0) { 75 | deltaNanos += 1000000000; 76 | deltaSecs -= 1; 77 | } 78 | return Math.trunc(deltaSecs * 1e6 + deltaNanos / 1e3); 79 | } 80 | 81 | function updatePercentage(gauge, currentUsed, prevUsed, totalMicros) { 82 | const delta = currentUsed - prevUsed; 83 | const percentage = delta / totalMicros * 100.0; 84 | gauge.update(percentage); 85 | } 86 | 87 | function toCamelCase(s) { 88 | return s.replace(/_([a-z])/g, function(g) { 89 | return g[1].toUpperCase(); 90 | }); 91 | } 92 | 93 | function updateV8HeapGauges(atlas, extraTags, heapStats) { 94 | for (const key of Object.keys(heapStats)) { 95 | const name = 'nodejs.' + toCamelCase(key); 96 | atlas.gauge(name, extraTags).update(heapStats[key]); 97 | } 98 | } 99 | 100 | function updateV8HeapSpaceGauges(atlas, extraTags, heapSpaceStats) { 101 | for (const space of heapSpaceStats) { 102 | const id = toCamelCase(space.space_name); 103 | 104 | for (const key of Object.keys(space)) { 105 | 106 | if (key !== 'space_name') { 107 | const name = 'nodejs.' + toCamelCase(key); 108 | const tags = Object.assign({ 109 | id: id 110 | }, extraTags); 111 | atlas.gauge(name, tags).update(space[key]); 112 | } 113 | } 114 | } 115 | } 116 | 117 | function refreshValues(self) { 118 | const memUsage = process.memoryUsage(); 119 | self.rss.update(memUsage.rss); 120 | self.heapTotal.update(memUsage.heapTotal); 121 | self.heapUsed.update(memUsage.heapUsed); 122 | self.external.update(memUsage.external); 123 | 124 | if (typeof process.cpuUsage === 'function') { 125 | const newCpuUsage = process.cpuUsage(); 126 | const newCpuUsageTime = process.hrtime(); 127 | 128 | const elapsedMicros = deltaMicros(newCpuUsageTime, self.lastCpuUsageTime); 129 | updatePercentage(self.cpuUsageUser, newCpuUsage.user, 130 | self.lastCpuUsage.user, elapsedMicros); 131 | updatePercentage(self.cpuUsageSystem, newCpuUsage.system, 132 | self.lastCpuUsage.system, elapsedMicros); 133 | 134 | self.lastCpuUsageTime = newCpuUsageTime; 135 | self.lastCpuUsage = newCpuUsage; 136 | } 137 | 138 | updateV8HeapGauges(self.atlas, self.extraTags, v8.getHeapStatistics()); 139 | 140 | if (typeof v8.getHeapSpaceStatistics === 'function') { 141 | updateV8HeapSpaceGauges(self.atlas, self.extraTags, 142 | v8.getHeapSpaceStatistics()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /doc/nodejs-metrics.md: -------------------------------------------------------------------------------- 1 | # Node.js metrics 2 | 3 | These set of metrics are automatically reported unless you opt-out. When the main 4 | application starts: 5 | 6 | ```js 7 | const atlas = require('atlasclient'); 8 | 9 | // main application only - not libraries 10 | atlas.start(); 11 | ``` 12 | 13 | You can disable these set of metrics by explicitly requesting `runtimeMetrics: false` 14 | 15 | ```js 16 | const atlas = require('atlasclient'); 17 | 18 | // main application only - not libraries 19 | atlas.start({runtimeMetrics: false}); 20 | ``` 21 | 22 | These Node.js metrics are tagged with the nodejs version. 23 | 24 | ## CPU 25 | Available only on node 6 and 7 26 | 27 | * nodejs.cpuUsage: Percentage of CPU time the node.js process is consuming 28 | [0, 100]: 29 | * id: user 30 | * id: system 31 | 32 | For example: 33 | ```js 34 | { 35 | "tags": { 36 | "id": "system", 37 | "name": "nodejs.cpuUsage", 38 | /// nf.* tags 39 | "nodejs.version": "v6.5.0" 40 | }, 41 | "start": 1485813720000, 42 | "value": 0.8954088417692685 43 | }, 44 | { 45 | "tags": { 46 | "id": "user", 47 | "name": "nodejs.cpuUsage", 48 | /// nf.* tags 49 | "nodejs.version": "v6.5.0" 50 | }, 51 | "start": 1485813720000, 52 | "value": 4.659007745141895 53 | } 54 | ``` 55 | 56 | ## Event Loop 57 | 58 | * `nodejs.eventLoop` timer, which measures the time it takes for the event loop 59 | to complete. This is measured using adaptive sampling (starting at twice per 60 | second, and backing off if the process is overloaded) 61 | 62 | * `nodejs.eventLoopLag` timer, which measures if the event loop 63 | is running behind by attempting to execute a timer once a second, and 64 | measuring the actual lag. 65 | 66 | 67 | ## Garbage Collection Metrics 68 | 69 | * `nodejs.gc.allocationRate`: measures in bytes/second how fast the app is 70 | allocating memory 71 | 72 | * `nodejs.gc.promotionRate`: measures in bytes/second how fast data is being 73 | moved from `new_space` to `old_space` 74 | 75 | * `nodejs.gc.liveDataSize`: measures in bytes the size of the `old_space` after 76 | a major GC event 77 | 78 | * `nodejs.gc.maxDataSize`: measures the maximum amount of memory the nodejs 79 | process is allowed to use. Primarly used for gaining perspective on the 80 | `liveDataSize`. 81 | 82 | * `nodejs.gc.pause`: times the time it takes for the different GC 83 | events: 84 | 85 | * `id=scavenge`: The most common garbage collection method. Node will 86 | typically trigger one of these every time the VM is idle. 87 | 88 | * `id=markSweepCompact`: The heaviest type of garbage collection V8 89 | may do. If you see many of these happening you will need to either 90 | keep fewer objects around in your process or increase V8's heap 91 | limit 92 | 93 | * `id=incrementalMarking`: A phased garbage collection that 94 | interleaves collection with application logic to reduce the amount 95 | of time the application is paused. 96 | 97 | * `id=processWeakCallbacks`: After a garbage collection occurs, V8 98 | will call any weak reference callbacks registered for objects that 99 | have been freed. This measurement is from the start of the first 100 | weak callback to the end of the last for a given garbage collection. 101 | 102 | ## Memory Usage Metrics 103 | 104 | Memory usage of the Node.js process in bytes: 105 | 106 | * rss: `nodejs.rss` 107 | * heapTotal: `nodejs.heapTotal` - v8 memory usage 108 | * heapUsed: `nodejs.heapUsed` - v8 memory usage 109 | * external: `nodejs.external` - memory usage of C++ objects bound to JS objects managed by v8 110 | 111 | ## V8 Heap Statistics 112 | * total_heap_size: `nodejs.totalHeapSize` 113 | * total_heap_size_executable: `nodejs.totalHeapSizeExecutable` 114 | * total_physical_size: `nodejs.totalPhysicalSize` 115 | * total_available_size: `nodejs.totalAvailableSize` 116 | * used_heap_size: `nodejs.usedHeapSize` 117 | * heap_size_limit: `nodejs.heapSizeLimit` 118 | * malloced_memory: `nodejs.mallocedMemory` 119 | * peak_malloced_memory: `nodejs.peakMallocedMemory` 120 | * does_zap_garbage: `nodejs.doesZapGarbage` - a 0/1 boolean, which signifies whether the --zap_code_space option is enabled or not. This makes V8 overwrite heap garbage with a bit pattern. The RSS footprint (resident memory set) gets bigger because it continuously touches all heap pages and that makes them less likely to get swapped out by the operating system. 121 | 122 | ## V8 Heap Space Statistics 123 | 124 | For each space returned by [v8.getHeapSpaceStatistics](https://nodejs.org/api/v8.html#v8_v8_getheapspacestatistics) 125 | 126 | * space_size: `nodejs.spaceSize` `id: ` 127 | * space_used_size: `nodejs.spaceUsedSize` `id: ` 128 | * space_available_size: `nodejs.spaceAvailableSize` `id: ` 129 | * physical_space_size: `nodejs.physicalSpaceSize` `id: ` 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let atlas; 4 | 5 | // See https://github.com/Netflix-Skunkworks/atlas-node-client/issues/25 for 6 | // more details. 7 | try { 8 | atlas = require('bindings')('atlas'); 9 | } catch (loadErr) { 10 | if (loadErr.message && 11 | /undefined symbol: [_\w]+atlas[_\w]+/.test(loadErr.message)) { 12 | console.error('!!! Incompatible atlasclient dependencies ' + 13 | 'detected! !!!'); 14 | console.error(''); 15 | console.error('This is likely the result of loading two ' + 16 | 'incompatible versions of the atlasclient module.'); 17 | console.error('Make sure all your atlasclient dependencies ' + 18 | 'specify the same version, or even better, that only your toplevel ' + 19 | 'application requires the atlasclient module'); 20 | console.error(''); 21 | console.error('!!! Incompatible atlasclient dependencies detected! !!!'); 22 | console.error(''); 23 | } 24 | throw loadErr; 25 | } 26 | 27 | let updateInterval = 30000; 28 | let nodeMetrics; 29 | 30 | const path = require('path'); 31 | 32 | atlas.JsTimer.prototype.timeAsync = function(fun) { 33 | const self = this; 34 | const start = process.hrtime(); 35 | const done = function() { 36 | self.record(process.hrtime(start)); 37 | }; 38 | return fun(done); 39 | }; 40 | 41 | let ageGaugeUpdateValue = function(ageGauge) { 42 | let elapsed = ((new Date).getTime() - ageGauge.lastUpdated) / 1000.0; 43 | ageGauge.gauge.update(elapsed); 44 | }; 45 | 46 | let AgeGauge = function(name, tags) { 47 | this.gauge = atlas.gauge(name, tags); 48 | this.lastUpdated = (new Date).getTime(); 49 | this.gauge.update(0); 50 | setInterval(ageGaugeUpdateValue, updateInterval, this); 51 | }; 52 | 53 | AgeGauge.prototype.value = function() { 54 | return this.gauge.value(); 55 | }; 56 | 57 | AgeGauge.prototype.update = function(updatedTime) { 58 | let t = updatedTime || (new Date).getTime(); 59 | this.lastUpdated = t; 60 | ageGaugeUpdateValue(this); 61 | }; 62 | 63 | let started = false; 64 | 65 | function startAtlas(config) { 66 | if (started) { 67 | return; 68 | } 69 | 70 | const cfg = config ? config : {}; 71 | // default log dirs 72 | let logDirs = ['/logs/atlas', path.join(__dirname, 'logs'), '/tmp']; 73 | 74 | if ('logDirs' in cfg) { 75 | logDirs = cfg.logDirs; 76 | } 77 | 78 | let runtimeMetrics = true; 79 | 80 | if ('runtimeMetrics' in cfg) { 81 | runtimeMetrics = cfg.runtimeMetrics; 82 | } 83 | 84 | let developmentMode = false; 85 | 86 | if ('developmentMode' in cfg) { 87 | developmentMode = cfg.developmentMode; 88 | } 89 | 90 | const nodeVersion = { 91 | 'nodejs.version': process.version 92 | }; 93 | atlas.start({ 94 | developmentMode: developmentMode, 95 | logDirs: logDirs, 96 | runtimeMetrics: runtimeMetrics, 97 | runtimeTags: nodeVersion 98 | }); 99 | 100 | if (runtimeMetrics) { 101 | const nm = require('./node-metrics'); 102 | nodeMetrics = new nm.NodeMetrics(atlas, nodeVersion); 103 | nodeMetrics.start(); 104 | } 105 | 106 | started = true; 107 | } 108 | 109 | function stopAtlas() { 110 | if (!started) { 111 | return; 112 | } 113 | 114 | if (nodeMetrics) { 115 | nodeMetrics.stop(); 116 | nodeMetrics = undefined; 117 | } 118 | atlas.stop(); 119 | started = false; 120 | } 121 | 122 | function debugInfo() { 123 | return { 124 | config: atlas.config(), 125 | measurements: atlas.measurements() 126 | }; 127 | } 128 | 129 | function devMode(enabled) { 130 | atlas.setDevMode(enabled); 131 | } 132 | 133 | function validateNameAndTags(name, tags) { 134 | return atlas.validateNameAndTags(name, tags); 135 | } 136 | 137 | let scope = function(commonTags) { 138 | let bucketArgs = function(name) { 139 | let tags, bucketFunction; 140 | 141 | if (arguments.length === 3) { 142 | tags = arguments[1]; 143 | bucketFunction = arguments[2]; 144 | } else { 145 | tags = {}; 146 | bucketFunction = arguments[1]; 147 | } 148 | 149 | return [name, Object.assign({}, commonTags, tags), bucketFunction]; 150 | }; 151 | 152 | let s = { 153 | start: startAtlas, 154 | stop: stopAtlas, 155 | setDevMode: devMode, 156 | getDebugInfo: debugInfo, 157 | validateNameAndTags: validateNameAndTags, 158 | counter: (name, tags) => atlas.counter( 159 | name, Object.assign({}, commonTags, tags)), 160 | dcounter: (name, tags) => atlas.dcounter( 161 | name, Object.assign({}, commonTags, tags)), 162 | intervalCounter: (name, tags) => atlas.intervalCounter( 163 | name, Object.assign({}, commonTags, tags)), 164 | timer: (name, tags) => atlas.timer( 165 | name, Object.assign({}, commonTags, tags)), 166 | gauge: (name, tags) => atlas.gauge( 167 | name, Object.assign({}, commonTags, tags)), 168 | maxGauge: (name, tags) => atlas.maxGauge( 169 | name, Object.assign({}, commonTags, tags)), 170 | distSummary: (name, tags) => atlas.distSummary( 171 | name, Object.assign({}, commonTags, tags)), 172 | longTaskTimer: (name, tags) => atlas.longTaskTimer( 173 | name, Object.assign({}, commonTags, tags)), 174 | percentileDistSummary: (name, tags) => atlas.percentileDistSummary( 175 | name, Object.assign({}, commonTags, tags)), 176 | percentileTimer: (name, tags) => atlas.percentileTimer( 177 | name, Object.assign({}, commonTags, tags)), 178 | age: (name, tags) => new AgeGauge( 179 | name, Object.assign({}, commonTags, tags)), 180 | bucketCounter: function() { 181 | let args = bucketArgs.apply(this, arguments); 182 | return atlas.bucketCounter(args[0], args[1], args[2]); 183 | }, 184 | bucketDistSummary: function() { 185 | let args = bucketArgs.apply(this, arguments); 186 | return atlas.bucketDistSummary(args[0], args[1], args[2]); 187 | }, 188 | bucketTimer: function() { 189 | let args = bucketArgs.apply(this, arguments); 190 | return atlas.bucketTimer(args[0], args[1], args[2]); 191 | }, 192 | measurements: () => atlas.measurements(), 193 | // used by the old prana interface. Should not be used by new code. 194 | // sends metrics immediately to atlas, they're not available for alerts/cloudwatch/etc. 195 | push: (metrics) => atlas.push(metrics), 196 | config: () => atlas.config(), 197 | scope: tags => scope(Object.assign({}, commonTags, tags)), 198 | // for testing 199 | setUpdateInterval: function(newInterval) { 200 | updateInterval = newInterval; 201 | } 202 | }; 203 | 204 | return s; 205 | }; 206 | 207 | module.exports = scope({}); 208 | -------------------------------------------------------------------------------- /src/functions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // enable/disable development mode 14 | NAN_METHOD(set_dev_mode); 15 | 16 | // perform validation checks on name, tags 17 | NAN_METHOD(validate_name_tags); 18 | 19 | // get an array of measurements intended for the main publish pipeline 20 | NAN_METHOD(measurements); 21 | // 22 | // get the current config 23 | NAN_METHOD(config); 24 | 25 | // push a list of measurements immediately to atlas 26 | NAN_METHOD(push); 27 | 28 | // get a counter 29 | NAN_METHOD(counter); 30 | 31 | // get a double counter 32 | NAN_METHOD(dcounter); 33 | 34 | // get an interval counter 35 | NAN_METHOD(interval_counter); 36 | 37 | // get a timer 38 | NAN_METHOD(timer); 39 | 40 | // get a long task timer 41 | NAN_METHOD(long_task_timer); 42 | 43 | // get a gauge 44 | NAN_METHOD(gauge); 45 | 46 | // get a max gauge 47 | NAN_METHOD(max_gauge); 48 | 49 | // get a distribution summary 50 | NAN_METHOD(dist_summary); 51 | 52 | // bucket counter 53 | NAN_METHOD(bucket_counter); 54 | 55 | // bucket distribution summary 56 | NAN_METHOD(bucket_dist_summary); 57 | 58 | // bucket timer 59 | NAN_METHOD(bucket_timer); 60 | 61 | // percentile timer 62 | NAN_METHOD(percentile_timer); 63 | 64 | // bucket timer 65 | NAN_METHOD(percentile_dist_summary); 66 | 67 | // wrapper for a counter 68 | class JsCounter : public Nan::ObjectWrap { 69 | public: 70 | static NAN_MODULE_INIT(Init); 71 | static Nan::Persistent constructor; 72 | 73 | private: 74 | explicit JsCounter(atlas::meter::IdPtr id); 75 | 76 | static NAN_METHOD(New); 77 | static NAN_METHOD(Increment); 78 | static NAN_METHOD(Add); 79 | static NAN_METHOD(Count); 80 | 81 | std::shared_ptr counter_; 82 | }; 83 | 84 | class JsDCounter : public Nan::ObjectWrap { 85 | public: 86 | static NAN_MODULE_INIT(Init); 87 | static Nan::Persistent constructor; 88 | 89 | private: 90 | explicit JsDCounter(atlas::meter::IdPtr id); 91 | 92 | static NAN_METHOD(New); 93 | static NAN_METHOD(Increment); 94 | static NAN_METHOD(Add); 95 | static NAN_METHOD(Count); 96 | 97 | std::shared_ptr counter_; 98 | }; 99 | 100 | // wrapper for a timer 101 | class JsTimer : public Nan::ObjectWrap { 102 | public: 103 | static NAN_MODULE_INIT(Init); 104 | static Nan::Persistent constructor; 105 | 106 | private: 107 | explicit JsTimer(atlas::meter::IdPtr id); 108 | 109 | static NAN_METHOD(New); 110 | static NAN_METHOD(Record); 111 | static NAN_METHOD(TimeThis); 112 | static NAN_METHOD(TotalTime); 113 | static NAN_METHOD(Count); 114 | 115 | std::shared_ptr timer_; 116 | }; 117 | 118 | // wrapper for a long task timer 119 | class JsLongTaskTimer : public Nan::ObjectWrap { 120 | public: 121 | static NAN_MODULE_INIT(Init); 122 | static Nan::Persistent constructor; 123 | 124 | private: 125 | explicit JsLongTaskTimer(atlas::meter::IdPtr id); 126 | 127 | static NAN_METHOD(New); 128 | static NAN_METHOD(Start); 129 | static NAN_METHOD(Stop); 130 | static NAN_METHOD(Duration); 131 | static NAN_METHOD(ActiveTasks); 132 | 133 | std::shared_ptr timer_; 134 | }; 135 | 136 | // wrapper for a gauge 137 | class JsGauge : public Nan::ObjectWrap { 138 | public: 139 | static NAN_MODULE_INIT(Init); 140 | static Nan::Persistent constructor; 141 | 142 | private: 143 | JsGauge(atlas::meter::IdPtr id); 144 | static NAN_METHOD(New); 145 | static NAN_METHOD(Value); 146 | static NAN_METHOD(Update); 147 | 148 | std::shared_ptr> gauge_; 149 | }; 150 | 151 | // wrapper for a max gauge 152 | class JsMaxGauge : public Nan::ObjectWrap { 153 | public: 154 | static NAN_MODULE_INIT(Init); 155 | static Nan::Persistent constructor; 156 | 157 | private: 158 | explicit JsMaxGauge(atlas::meter::IdPtr id); 159 | 160 | static NAN_METHOD(New); 161 | static NAN_METHOD(Update); 162 | static NAN_METHOD(Value); 163 | 164 | std::shared_ptr> max_gauge_; 165 | }; 166 | 167 | class JsDistSummary : public Nan::ObjectWrap { 168 | public: 169 | static NAN_MODULE_INIT(Init); 170 | static Nan::Persistent constructor; 171 | 172 | private: 173 | explicit JsDistSummary(atlas::meter::IdPtr id); 174 | 175 | static NAN_METHOD(New); 176 | static NAN_METHOD(Record); 177 | static NAN_METHOD(TotalAmount); 178 | static NAN_METHOD(Count); 179 | 180 | std::shared_ptr dist_summary_; 181 | }; 182 | 183 | class JsBucketCounter : public Nan::ObjectWrap { 184 | public: 185 | static NAN_MODULE_INIT(Init); 186 | static Nan::Persistent constructor; 187 | 188 | private: 189 | explicit JsBucketCounter(atlas::meter::IdPtr id, 190 | atlas::meter::BucketFunction bucket_function); 191 | 192 | static NAN_METHOD(New); 193 | static NAN_METHOD(Record); 194 | 195 | std::shared_ptr bucket_counter_; 196 | }; 197 | 198 | class JsBucketDistSummary : public Nan::ObjectWrap { 199 | public: 200 | static NAN_MODULE_INIT(Init); 201 | static Nan::Persistent constructor; 202 | 203 | private: 204 | explicit JsBucketDistSummary(atlas::meter::IdPtr id, 205 | atlas::meter::BucketFunction bucket_function); 206 | 207 | static NAN_METHOD(New); 208 | static NAN_METHOD(Record); 209 | 210 | std::shared_ptr bucket_dist_summary_; 211 | }; 212 | 213 | class JsBucketTimer : public Nan::ObjectWrap { 214 | public: 215 | static NAN_MODULE_INIT(Init); 216 | static Nan::Persistent constructor; 217 | 218 | private: 219 | explicit JsBucketTimer(atlas::meter::IdPtr id, 220 | atlas::meter::BucketFunction bucket_function); 221 | 222 | static NAN_METHOD(New); 223 | static NAN_METHOD(Record); 224 | 225 | std::shared_ptr bucket_timer_; 226 | }; 227 | 228 | class JsPercentileTimer : public Nan::ObjectWrap { 229 | public: 230 | static NAN_MODULE_INIT(Init); 231 | static Nan::Persistent constructor; 232 | 233 | private: 234 | explicit JsPercentileTimer(atlas::meter::IdPtr id); 235 | 236 | static NAN_METHOD(New); 237 | static NAN_METHOD(Record); 238 | static NAN_METHOD(Percentile); 239 | static NAN_METHOD(TotalTime); 240 | static NAN_METHOD(Count); 241 | 242 | std::shared_ptr perc_timer_; 243 | }; 244 | 245 | class JsPercentileDistSummary : public Nan::ObjectWrap { 246 | public: 247 | static NAN_MODULE_INIT(Init); 248 | static Nan::Persistent constructor; 249 | 250 | private: 251 | explicit JsPercentileDistSummary(atlas::meter::IdPtr id); 252 | 253 | static NAN_METHOD(New); 254 | static NAN_METHOD(Record); 255 | static NAN_METHOD(Percentile); 256 | static NAN_METHOD(TotalAmount); 257 | static NAN_METHOD(Count); 258 | 259 | std::shared_ptr 260 | perc_dist_summary_; 261 | }; 262 | 263 | class JsIntervalCounter : public Nan::ObjectWrap { 264 | public: 265 | static NAN_MODULE_INIT(Init); 266 | static Nan::Persistent constructor; 267 | 268 | private: 269 | explicit JsIntervalCounter(atlas::meter::IdPtr id); 270 | 271 | static NAN_METHOD(New); 272 | static NAN_METHOD(Increment); 273 | static NAN_METHOD(Add); 274 | static NAN_METHOD(Count); 275 | static NAN_METHOD(SecondsSinceLastUpdate); 276 | 277 | std::shared_ptr counter_; 278 | }; 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atlas Client for Node.js 2 | 3 | [![Snapshot](https://github.com/Netflix-Skunkworks/atlas-node-client/actions/workflows/snapshot.yml/badge.svg)](https://github.com/Netflix-Skunkworks/atlas-node-client/actions/workflows/snapshot.yml) [![Release](https://github.com/Netflix-Skunkworks/atlas-node-client/actions/workflows/release.yml/badge.svg)](https://github.com/Netflix-Skunkworks/atlas-node-client/actions/workflows/release.yml) 4 | 5 | > :warning: This library is deprecated. Javascript projects should migrate to 6 | [spectator-js](https://github.com/Netflix/spectator-js) for publishing Atlas 7 | metrics. 8 | 9 | Module for generating metrics and sending them to Atlas. 10 | 11 | ## Local Development (MacOS) 12 | 13 | ``` 14 | brew install node@14 15 | export PATH="/usr/local/opt/node@14/bin:$PATH" 16 | make 17 | ``` 18 | 19 | ## Getting Started 20 | 21 | Install the module with: `npm install atlasclient` 22 | 23 | ## Usage 24 | 25 | ```js 26 | const atlas = require("atlasclient"); 27 | atlas.start(); // only the main application, not libraries instrumenting code 28 | 29 | //... 30 | app.get("/somehandler", (req, res) => { 31 | let start = process.hrtime(); 32 | // ... 33 | // do the work 34 | // statusCode is set here 35 | let elapsed = process.hrtime(start); 36 | atlas.timer('server.requestLatency').record(elapsed); 37 | atlas.counter('server.requestCount', { 38 | country: getCountry(req), 39 | status: statusCode 40 | }).increment(); 41 | 42 | let responseSize = getResponseSize(res); 43 | atlas.distSummary('server.responseSize').record(responseSize); 44 | }); 45 | 46 | // only for the main application, not libraries 47 | process.on('beforeExit', () => { 48 | atlas.stop(); // FIXME 49 | }); 50 | ``` 51 | 52 | ## Starting and Stopping 53 | 54 | In your main application call `atlas.start()`. 55 | 56 | If you wish to opt-out of [Node.js runtime metrics](doc/nodejs-metrics.md), pass `{runtimeMetrics: false}` to the start method. 57 | 58 | ## Instrumenting Code 59 | 60 | See the usage guides for [counters](doc/counter.md), [timers](doc/timer.md), [gauges](doc/gauge.md), 61 | and [distribution summaries](doc/dist-summary.md). 62 | 63 | In addition to the basic meters, you can also choose to use more expensive meters that allow you to 64 | get an idea of the distribution of samples: 65 | 66 | * [buckets](doc/buckets.md) 67 | * [percentiles](doc/percentiles.md) 68 | 69 | ## Unit Testing 70 | 71 | See the [test] directory for examples of unit testing. These tests can be run with `npm test`. 72 | 73 | [test]: https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/browse/test 74 | 75 | ## Development Mode 76 | 77 | When developing it's sometimes useful to throw errors when constructing invalid 78 | metrics. By default the client will ignore these errors so it doesn't affect a 79 | running application in production, but you can change this behavior by enabling 80 | the development mode: 81 | 82 | `atlas.setDevMode(true);` 83 | 84 | This will validate metrics can be successfully sent to atlas, and throw in case of errors. 85 | 86 | ## Debugging 87 | 88 | * Configuration for the atlas-native-client, the dependency of the atlas-node-client that is responsible 89 | for publishing metrics to the Atlas backend, is centrally controlled, but can be overridden with the file 90 | `/usr/local/etc/atlas-config.json`. 91 | * Enable verbose logging in atlas-native-client by adding `{ "logVerbosity": 1 }` to `atlas-config.json`. 92 | Possible log file directories are listed below; the first that can be written to is used. The generated 93 | directory path is typically `$PWD/node_modules/atlasclient/logs`. 94 | ```js 95 | let logDirs = ['/logs/atlas', path.join(__dirname, 'logs'), '/tmp']; 96 | ``` 97 | * The `validateMetrics` option in atlas-native-client defaults to true. It can be overridden by 98 | adding `"validateMetrics": false` to the `atlas-config.json` file. 99 | * `config()` Dump the internal configuration of the atlasclient package. The `commonTags` are 100 | extracted from environment variables. The `dumpMetrics` option controls whether or not the exact 101 | payload sent to the Atlas backend is logged. 102 | 103 | ```js 104 | { evaluateUrl: 'http://atlas-lwcapi-iep.us-east-1.ieptest.netflix.net/lwc/api/v1/evaluate', 105 | subscriptionsUrl: 'http://atlas-lwcapi-iep.us-east-1.ieptest.netflix.net/lwc/api/v1/expressions/go2-client', 106 | publishUrl: 'http://atlas-pub-179727101194.us-east-1.ieptest.netflix.net/api/v1/publish-fast', 107 | subscriptionsRefreshMillis: 10000, 108 | batchSize: 10000, 109 | connectTimeout: 1, 110 | readTimeout: 10, 111 | dumpMetrics: false, 112 | dumpSubscriptions: false, 113 | publishEnabled: true, 114 | publishConfig: [ ':true,:all' ], 115 | commonTags: 116 | { 'nf.account': '179727101194', 117 | 'nf.ami': 'ami-be748aa8', 118 | 'nf.app': 'go2', 119 | 'nf.asg': 'go2-client-v012', 120 | 'nf.cluster': 'go2-client', 121 | 'nf.node': 'i-0feaad16fc792bca6', 122 | 'nf.region': 'us-east-1', 123 | 'nf.vmtype': 'm3.medium', 124 | 'nf.zone': 'us-east-1c' } } 125 | ``` 126 | 127 | * `measurements()` This can be used to get a list of the current measurements. The return result 128 | is an array of objects with two properties: `tags` and `value`. 129 | 130 | ```js 131 | [ { tags: 132 | { name: 'atlas.client.mainMeasurements', 133 | 'nf.account': '179727101194', 134 | 'nf.ami': 'ami-be748aa8', 135 | 'nf.app': 'go2', 136 | 'nf.asg': 'go2-client-v012', 137 | 'nf.cluster': 'go2-client', 138 | 'nf.node': 'i-0feaad16fc792bca6', 139 | 'nf.region': 'us-east-1', 140 | 'nf.vmtype': 'm3.medium', 141 | 'nf.zone': 'us-east-1c' }, 142 | value: 3 }, 143 | { tags: 144 | { name: 'atlas.client.meters', 145 | 'nf.account': '179727101194', 146 | 'nf.ami': 'ami-be748aa8', 147 | 'nf.app': 'go2', 148 | 'nf.asg': 'go2-client-v012', 149 | 'nf.cluster': 'go2-client', 150 | 'nf.node': 'i-0feaad16fc792bca6', 151 | 'nf.region': 'us-east-1', 152 | 'nf.vmtype': 'm3.medium', 153 | 'nf.zone': 'us-east-1c' }, 154 | value: 3 }, 155 | { tags: 156 | { name: 'atlas.client.rawMainMeasurements', 157 | 'nf.account': '179727101194', 158 | 'nf.ami': 'ami-be748aa8', 159 | 'nf.app': 'go2', 160 | 'nf.asg': 'go2-client-v012', 161 | 'nf.cluster': 'go2-client', 162 | 'nf.node': 'i-0feaad16fc792bca6', 163 | 'nf.region': 'us-east-1', 164 | 'nf.vmtype': 'm3.medium', 165 | 'nf.zone': 'us-east-1c' }, 166 | value: 3 } ] 167 | ``` 168 | 169 | ## Internal 170 | 171 | * `push(measurements)` 172 | 173 | Send the array of measurements immediately to the server, without adding any common tags. 174 | 175 | A measurement is an object with three properties: 176 | 177 | * `start`: timestamp in milliseconds 178 | * `tags`: object describing how to tag the measurement. `name` is required. 179 | * `value`: number 180 | 181 | ## Help, this doesn't work on Node 8 with Babel? 182 | 183 | If you receive an error like: 184 | 185 | ``` 186 | /Users/sdhillon/projects/quitelite/node_modules/core-js/modules/_typed-buffer.js:157 187 | if(numberLength != byteLength)throw RangeError(WRONG_LENGTH); 188 | ^ 189 | 190 | RangeError: Wrong length! 191 | at validateArrayBufferArguments (/Users/sdhillon/projects/quitelite/node_modules/core-js/modules/_typed-buffer.js:157:39) 192 | at new ArrayBuffer (/Users/sdhillon/projects/quitelite/node_modules/core-js/modules/_typed-buffer.js:247:29) 193 | at v8.js:132:17 194 | at NativeModule.compile (bootstrap_node.js:563:7) 195 | at Function.NativeModule.require (bootstrap_node.js:506:18) 196 | at Function.Module._load (module.js:446:25) 197 | at Module.require (module.js:513:17) 198 | at require (internal/module.js:11:18) 199 | at repl:1:-61 200 | at ContextifyScript.Script.runInThisContext (vm.js:44:33) 201 | ``` 202 | 203 | On Node 8, core-js does not properly support array buffer construction. Since we pull 204 | metrics from v8, and v8 relys on `ArrayBuffer`, this does not work at the moment. Fortunately, 205 | when starting Atlas, you can disable this if you're hitting this issue. `start(...)` takes 206 | the following configuration options: 207 | 208 | ``` 209 | { 210 | runtimeMetrics: boolean, // default: true 211 | logDirs: Array, // default: ['/logs/atlas', path.join(__dirname, 'logs'), '/tmp']; 212 | } 213 | ``` 214 | 215 | If you disable runtimeMetrics, atlas client will not load v8, and therefore 216 | it will not automatically generate metrics like: 217 | 218 | * nodejs.rss 219 | * nodejs.heapTotal 220 | * nodejs.heapUsed 221 | * nodejs.external 222 | 223 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.23.10 (2019-04-22) 2 | 3 | * native-client 0.5.7 fixes honoring the env var for disabled file 4 | 5 | ## 1.23.9 (2019-03-13) 6 | 7 | #### Update 8 | 9 | * native-client 0.5.6 for fixes around validation errors 10 | 11 | ## 1.23.8 (2018-10-11) 12 | 13 | #### Update 14 | 15 | * native-client 0.5.4 for fixes around attributing increments to the correct 16 | bucket 17 | 18 | ## 1.23.7 (2018-10-09) 19 | 20 | #### Update 21 | 22 | * Uses native client 0.5.3 to use step numbers in the consolidation registry. 23 | This allows it to correctly attribute the update to a given step. 24 | 25 | ## 1.23.6 (2018-10-04) 26 | 27 | #### Update 28 | 29 | * Uses native client 0.5.2 to get a fix for flushing metrics at shutdown 30 | 31 | ## 1.23.5 (2018-09-06) 32 | 33 | #### Update 34 | 35 | * Uses native client 0.5.1 to get a fix for handling the case where the user registers an Id with a type, and then attempts to register the same Id with a different type 36 | * Note that native client 0.5.x uses a consolidation registry so there might be some slight change in the behavior of certain metrics 37 | 38 | ## 1.23.4 (2018-07-27) 39 | 40 | #### Fix 41 | 42 | * Output a better error message when failing to load the binary module 43 | 44 | ## 1.23.3 (2018-06-29) 45 | 46 | #### Update 47 | 48 | * Uses native client 0.4.5 which removes the sending of notifications to the alert server 49 | about lack of on-instance alerts support 50 | 51 | ## 1.23.2 (2018-05-22) 52 | 53 | #### Update 54 | 55 | * Uses native client 0.4.4 and its `AnalyzeTags` helper function by exposing 56 | a `validateNameAndTags()` function that takes a name, and tags to keep the interface 57 | consistent with the meter functions. 58 | 59 | ## 1.23.1 (2018-05-16) 60 | 61 | #### Update 62 | 63 | * Uses native client 0.4.3 (new versioning) which expires meters from the 64 | registry, and includes some minor performance improvements. 65 | 66 | ## 1.23.0 (2018-05-04) 67 | 68 | #### Update 69 | 70 | * Uses native client 4.0.1 which includes improvements to the API, fixes for 71 | subscriptions (lwc) and performance improvements. 72 | 73 | #### Fix 74 | ## 1.22.1 (2018-03-26) 75 | 76 | #### Fix 77 | 78 | * Uses native client 3.7.3 which removes the use of sleep allowing the client 79 | to be started and stopped quickly (in integration tests for example), plus 80 | some other bug fixes. 81 | 82 | ## 1.22.0 (2018-03-21) 83 | 84 | #### Fix 85 | 86 | * Make sure we do not throw errors in production 87 | 88 | ## 1.21.1 (2018-03-09) 89 | 90 | #### Fix 91 | 92 | * Make sure metrics always have a valid name. 93 | 94 | ## 1.21.0 (2018-03-06) 95 | 96 | #### New 97 | 98 | * `atlas.setDevMode(true)` will enable metrics validation at creation time, 99 | throwing errors in case of issues. 100 | 101 | ## 1.20.0 (2018-03-05) 102 | 103 | #### New 104 | 105 | * libatlasclient (v3.7.2) exposes dcounter. A counter that takes floating point increments 106 | 107 | ## 1.19.2 (2013-03-02) 108 | 109 | #### Fix 110 | 111 | * Removes unneeded artifacts from binary blob 112 | 113 | ## 1.19.1 (2018-03-01) 114 | 115 | #### Update 116 | 117 | * libatlasclient (v3.7.1): fixes locking bugs around logging, and fixes some threading issues. 118 | 119 | ## 1.19.0 (2018-02-28) 120 | 121 | #### Update 122 | * libatlasclient (v3.7.0): new config api 123 | 124 | #### Fix 125 | * Remove unneeded artifacts from build/Release directory to trim size of the module 126 | 127 | ## 1.18.2 (2017-11-22) 128 | 129 | #### Update 130 | 131 | * libatlasclient (v3.6.2): option to disable validation, plus other improvements. 132 | 133 | ## 1.18.0 (2017-11-20) 134 | 135 | #### Update 136 | 137 | * libatlasclient (v3.6.1): faster tags implementation, and improvements to memory usage. 138 | 139 | ## 1.17.0 (2017-10-17) 140 | 141 | #### Update 142 | 143 | * libatlasclient (v3.4.0): control enabling/disabling of metrics using a file 144 | (used by the traffic team internally), plus some hardening efforts. 145 | 146 | #### Fix 147 | 148 | * `nodejs.gc.promotionRate` was sometimes negative. 149 | 150 | #### Chore 151 | 152 | * Travis setup 153 | 154 | ## 1.16.2 (2017-09-19) 155 | 156 | * libatlasclient (v3.1.0): memory management improvements and better handling of large number of metrics 157 | 158 | ## 1.14.0 (2017-09-05) 159 | 160 | #### Fix 161 | 162 | 163 | * Update the usage of the `max_gauge` API. The 2.2.0 release of the 164 | native library is binary incompatible with previous versions due to a change 165 | of the `registry->max_gauge` return value. This call was broken in previous 166 | versions. 167 | 168 | ## 1.13.1 (2017-08-31) 169 | 170 | #### Fix 171 | 172 | * Latest libatlasclient (2.1.2) fixes default set of common tags 173 | 174 | ## 1.13.0 (2017-08-28) 175 | 176 | #### Fix 177 | 178 | * Latest libatlasclient (2.1.1) fixes the statistic count used by percentile timers 179 | 180 | #### New 181 | 182 | * If runtime metrics are enabled, the client will automatically track open and max file descriptors for the current process 183 | 184 | ## 1.12.0 (2017-08-10) 185 | 186 | #### Update 187 | 188 | * latest libatlasclient comes with a breaking API change but significant performance improvements 189 | 190 | #### Fix 191 | 192 | * Switch to a more accurate measure of the nodejs eventloop 193 | 194 | ## 1.11.1 (2017-08-02) 195 | 196 | #### Update 197 | 198 | * latest libatlasclient to get a bit more perf. when sending metrics 199 | 200 | ## 1.11.0 (2017-08-02) 201 | 202 | #### Fix 203 | 204 | * Depend on latest libatlasclient to get performance improvements related to sending metrics 205 | * Match the Spectator tagging behavior for `intervalCounter` 206 | 207 | ## 1.10.0 (2017-07-31) 208 | 209 | #### Fix 210 | 211 | * Fix push functionality 212 | 213 | ## 1.10.0 (2017-06-09) 214 | 215 | #### New 216 | 217 | * Adds nf.task as a common tag (by depending on latest libatlasclient build) 218 | 219 | ## 1.9.2 (2017-04-24) 220 | 221 | #### Fix 222 | 223 | * Fix rpath on OSX ([b4339aa](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/b4339aa5871358b319765f95d7fe0f5843aa9ea9)) 224 | 225 | ## 1.9.1 (2017-04-24) 226 | 227 | #### Fix 228 | 229 | * Default log dir is /logs/atlas ([0e44a1f](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/0e44a1fb45091a53eaf48014d6c7c5eb2d0f6a9f)) 230 | * Do not filter lag for the event loop ([d839bde](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/d839bde2ef8db709b00e6a5f6fa9219d06326a7c)) 231 | * Perform empty checks for names, keys, values ([823ebd2](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/823ebd236743cddc6f2b520bf84681b0da29b9b0)) 232 | 233 | #### Chore 234 | 235 | * changelog tool now links to actual commits ([98a6351](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/98a63518093e120fa1ec581b32a825e61420eae5)) 236 | 237 | ## 1.9.0 (2017-04-19) 238 | 239 | #### Fix 240 | 241 | * Refresh the liveDataSize to avoid expiration ([6d68565](https://stash.corp.netflix.com/projects/CLDMTA/repos/atlas-node-client/commits/6d68565d6483f258ada35da571c2a2dc03b3843d)) 242 | 243 | ## 1.8.0 (2017-04-19) 244 | 245 | #### New 246 | 247 | * Event loop and GC metrics 248 | 249 | * Timer: `nodejs.eventLoop` which measures the time it takes for the event loop to complete (using adaptive sampling) 250 | 251 | * Timer: `nodejs.eventLoopLag` which measures the lag nodejs has in executing a timer every 500ms. It should be 0, unless the nodejs process is too busy. 252 | 253 | * Timers: `nodejs.gc.pause` (`id=scavenge`, `id=incrementalMarking`, `id=markSweepCompact`, `id=processWeakCallbacks`), which measure the different v8 gc events 254 | 255 | * Counter: `nodejs.gc.allocationRate` measures in bytes/second the rate at which new memory is being allocated 256 | 257 | * Counter: `nodejs.gc.promotionRate` measures in bytes/second the rate at which memory is promoted to the old-space 258 | 259 | * Gauge: `nodejs.gc.liveDataSize` size in bytes of the old-space after a major GC event 260 | 261 | * Gauge: `nodejs.gc.maxDataSize` maximum size allowed for the heap 262 | 263 | * Mocks 264 | 265 | * `require('atlasclient/mocks')` 266 | 267 | ## 1.7.0 268 | * Provides a new config option to `atlas.start`: `atlas.start({logDirs: ['/foo/logs', '/bar/logs']})` 269 | default is: `['/logs', __dirname + '/logs']`. If we can't write to any directory passed, we write to /tmp. 270 | If we can't write to /tmp, then we log to stderr. 271 | * Add simple skeleton for mocks: `require('atlasclient/mocks')` - Will complete mocks in a subsequent release. 272 | 273 | ## 1.6.0 274 | * Provides `getDebugInfo` method 275 | 276 | * Provides `intervalCounter`: a counter that publishes its rate per second, 277 | plus the time since its last update 278 | 279 | ## 1.5.6 280 | * Uses libatlasclient 1.5.2, which fixes shutdown issues 281 | 282 | ## 1.5.1 - 1.5.5 283 | * Attempts to fix some compilation issues on platforms without pre-built 284 | binaries 285 | 286 | ## 1.5.0 287 | * Fixes crash on timeout due to libcurl - back to node-pre-gyp method. 288 | 289 | ## 1.4.0 290 | * Rollback to old compilation method due to a problem with the pre-packaged libcurl 291 | 292 | ## 1.3.0 293 | * Installation via node-pre-gyp using a new artifactory repository for atlas-client 294 | 295 | -------------------------------------------------------------------------------- /src/start_stop.cc: -------------------------------------------------------------------------------- 1 | #include "start_stop.h" 2 | #include "atlas.h" 3 | #include "utils.h" 4 | #include 5 | #include 6 | 7 | using atlas::meter::Counter; 8 | using atlas::meter::Gauge; 9 | using atlas::meter::IdPtr; 10 | using atlas::meter::Registry; 11 | using atlas::meter::Tag; 12 | using atlas::meter::Tags; 13 | using atlas::meter::Timer; 14 | using v8::GCType; 15 | using v8::HeapSpaceStatistics; 16 | 17 | static bool started = false; 18 | static bool timer_started = false; 19 | static uv_timer_t lag_timer; 20 | static uv_timer_t fd_timer; 21 | static int64_t prev_timestamp; 22 | static constexpr unsigned int POLL_PERIOD_MS = 500; 23 | static constexpr unsigned int FD_PERIOD_MS = 30 * 1000; 24 | static constexpr int64_t POLL_PERIOD_NS = POLL_PERIOD_MS * 1000000; 25 | static Tags runtime_tags; 26 | 27 | // how far behind are we in scheduling a periodic timer 28 | static std::shared_ptr atlas_lag_timer; 29 | static std::shared_ptr> open_fd_gauge; 30 | static std::shared_ptr> max_fd_gauge; 31 | 32 | static void record_lag(uv_timer_t* handle) { 33 | auto now = atlas_registry()->clock().MonotonicTime(); 34 | // compute lag 35 | if (prev_timestamp == 0) { 36 | prev_timestamp = now; 37 | return; 38 | } 39 | 40 | auto elapsed = now - prev_timestamp; 41 | auto lag = elapsed - POLL_PERIOD_NS; 42 | prev_timestamp = now; 43 | atlas_lag_timer->Record(lag); 44 | } 45 | 46 | static size_t get_dir_count(const char* dir) { 47 | auto fd = opendir(dir); 48 | if (fd == nullptr) { 49 | return 0; 50 | } 51 | size_t count = 0; 52 | struct dirent* dp; 53 | while ((dp = readdir(fd)) != nullptr) { 54 | if (dp->d_name[0] == '.') { 55 | // ignore hidden files (including . and ..) 56 | continue; 57 | } 58 | ++count; 59 | } 60 | closedir(fd); 61 | return count; 62 | } 63 | 64 | static void record_fd_activity(uv_timer_t* handle) { 65 | auto fd_count = get_dir_count("/proc/self/fd"); 66 | struct rlimit rl; 67 | getrlimit(RLIMIT_NOFILE, &rl); 68 | 69 | open_fd_gauge->Update(fd_count); 70 | max_fd_gauge->Update(rl.rlim_cur); 71 | } 72 | 73 | static HeapSpaceStatistics* beforeStats; 74 | static int64_t startGC; 75 | static std::shared_ptr alloc_rate_counter; 76 | static std::shared_ptr promotion_rate_counter; 77 | static std::shared_ptr> live_data_size; 78 | static std::shared_ptr> max_data_size; 79 | 80 | static std::unordered_map> gc_timers; 81 | 82 | inline IdPtr node_id(const char* name) { 83 | return atlas_registry()->CreateId(name, runtime_tags); 84 | } 85 | 86 | static void create_memory_meters(Registry* r) { 87 | alloc_rate_counter = r->counter(node_id("nodejs.gc.allocationRate")); 88 | promotion_rate_counter = r->counter(node_id("nodejs.gc.promotionRate")); 89 | live_data_size = r->gauge(node_id("nodejs.gc.liveDataSize")); 90 | max_data_size = r->gauge(node_id("nodejs.gc.maxDataSize")); 91 | } 92 | 93 | static void create_gc_timers(Registry* r) { 94 | auto base_id = node_id("nodejs.gc.pause"); 95 | gc_timers[v8::kGCTypeScavenge] = 96 | r->timer(base_id->WithTag(Tag::of("id", "scavenge"))); 97 | gc_timers[v8::kGCTypeMarkSweepCompact] = 98 | r->timer(base_id->WithTag(Tag::of("id", "markSweepCompact"))); 99 | gc_timers[v8::kGCTypeIncrementalMarking] = 100 | r->timer(base_id->WithTag(Tag::of("id", "incrementalMarking"))); 101 | gc_timers[v8::kGCTypeProcessWeakCallbacks] = 102 | r->timer(base_id->WithTag(Tag::of("id", "processWeakCallbacks"))); 103 | } 104 | 105 | static std::shared_ptr get_gc_timer(GCType type) { 106 | auto int_type = static_cast(type); 107 | auto it = gc_timers.find(int_type); 108 | if (it != gc_timers.end()) { 109 | return it->second; 110 | } 111 | 112 | // Unknown GC type - should never happen with node 6.x or 7.x 113 | return atlas_registry()->timer( 114 | node_id("nodejs.gc.pause") 115 | ->WithTag(Tag::of("id", std::to_string(int_type)))); 116 | } 117 | 118 | inline size_t number_heap_spaces() { 119 | auto isolate = v8::Isolate::GetCurrent(); 120 | return isolate->NumberOfHeapSpaces(); 121 | } 122 | 123 | inline HeapSpaceStatistics* alloc_heap_stats() { 124 | auto n = number_heap_spaces(); 125 | auto stats = new HeapSpaceStatistics[n]; 126 | memset(stats, 0, sizeof(HeapSpaceStatistics) * n); 127 | return stats; 128 | } 129 | 130 | inline void free_heap_stats(HeapSpaceStatistics* stats) { delete[] stats; } 131 | 132 | static bool fill_heap_stats(HeapSpaceStatistics* stats) { 133 | bool ok = true; 134 | auto n = number_heap_spaces(); 135 | auto isolate = v8::Isolate::GetCurrent(); 136 | for (size_t i = 0; i < n; ++i) { 137 | if (!isolate->GetHeapSpaceStatistics(&stats[i], i)) { 138 | ok = false; 139 | } 140 | } 141 | return ok; 142 | } 143 | 144 | static NAN_GC_CALLBACK(beforeGC) { 145 | startGC = atlas_registry()->clock().MonotonicTime(); 146 | fill_heap_stats(beforeStats); 147 | } 148 | 149 | static int find_space_idx(HeapSpaceStatistics* stats, const char* name) { 150 | auto n = static_cast(number_heap_spaces()); 151 | for (auto i = 0; i < n; ++i) { 152 | auto space_name = stats[i].space_name(); 153 | if (strcmp(space_name, name) == 0) { 154 | return i; 155 | } 156 | } 157 | return -1; 158 | } 159 | 160 | static NAN_GC_CALLBACK(afterGC) { 161 | auto elapsed = atlas_registry()->clock().MonotonicTime() - startGC; 162 | get_gc_timer(type)->Record(elapsed); 163 | 164 | auto stats = alloc_heap_stats(); 165 | fill_heap_stats(stats); 166 | 167 | v8::HeapStatistics heapStats; 168 | Nan::GetHeapStatistics(&heapStats); 169 | max_data_size->Update(heapStats.heap_size_limit()); 170 | 171 | auto old_space_idx = find_space_idx(beforeStats, "old_space"); 172 | bool live_data_updated = false; 173 | 174 | if (old_space_idx >= 0) { 175 | size_t old_before = beforeStats[old_space_idx].space_used_size(); 176 | size_t old_after = stats[old_space_idx].space_used_size(); 177 | int64_t delta = static_cast(old_after) - old_before; 178 | 179 | if (delta > 0) { 180 | promotion_rate_counter->Add(delta); 181 | } 182 | 183 | if (old_after < old_before || type == v8::kGCTypeMarkSweepCompact) { 184 | live_data_updated = true; 185 | live_data_size->Update(old_after); 186 | } 187 | } 188 | 189 | auto new_space_idx = find_space_idx(beforeStats, "new_space"); 190 | if (new_space_idx >= 0) { 191 | auto young_before = 192 | static_cast(beforeStats[new_space_idx].space_used_size()); 193 | auto young_after = 194 | static_cast(stats[new_space_idx].space_used_size()); 195 | auto delta = young_before - young_after; 196 | alloc_rate_counter->Add(delta); 197 | } 198 | free_heap_stats(stats); 199 | 200 | if (!live_data_updated) { 201 | // refresh the last-updated time 202 | live_data_size->Update(live_data_size->Value()); 203 | } 204 | } 205 | 206 | NAN_METHOD(start) { 207 | if (started) { 208 | return; 209 | } 210 | 211 | std::vector log_dirs; 212 | if (info.Length() == 1 && info[0]->IsObject()) { 213 | auto isolate = info.GetIsolate(); 214 | auto context = isolate->GetCurrentContext(); 215 | const auto& options = info[0].As(); 216 | const auto& logDirsKey = Nan::New("logDirs").ToLocalChecked(); 217 | const auto& runtimeMetricsKey = Nan::New("runtimeMetrics").ToLocalChecked(); 218 | const auto& runtimeTagsKey = Nan::New("runtimeTags").ToLocalChecked(); 219 | const auto& devModeKey = Nan::New("developmentMode").ToLocalChecked(); 220 | 221 | auto maybeLogDirs = options->Get(context, logDirsKey); 222 | if (!maybeLogDirs.IsEmpty()) { 223 | auto input_log_dirs = maybeLogDirs.ToLocalChecked().As(); 224 | const auto n = input_log_dirs->Length(); 225 | for (size_t i = 0; i < n; ++i) { 226 | auto dir = input_log_dirs->Get(context, i).ToLocalChecked(); 227 | const auto& path = std::string(*Nan::Utf8String(dir)); 228 | log_dirs.push_back(path); 229 | } 230 | } 231 | 232 | auto maybe_runtime_tags = options->Get(context, runtimeTagsKey); 233 | if (!maybe_runtime_tags.IsEmpty()) { 234 | std::string err_msg; 235 | tagsFromObject(isolate, 236 | maybe_runtime_tags.ToLocalChecked().As(), 237 | &runtime_tags, &err_msg); 238 | } 239 | 240 | auto maybeRuntimeMetrics = options->Get(context, runtimeMetricsKey); 241 | if (!maybeRuntimeMetrics.IsEmpty()) { 242 | auto runtimeMetrics = 243 | maybeRuntimeMetrics.ToLocalChecked().As()->Value(); 244 | if (runtimeMetrics) { 245 | auto r = atlas_registry(); 246 | // setup lag timer 247 | atlas_lag_timer = r->timer(node_id("nodejs.eventLoopLag")); 248 | uv_timer_init(uv_default_loop(), &lag_timer); 249 | uv_timer_start(&lag_timer, record_lag, POLL_PERIOD_MS, POLL_PERIOD_MS); 250 | 251 | uv_timer_init(uv_default_loop(), &fd_timer); 252 | uv_timer_start(&fd_timer, record_fd_activity, FD_PERIOD_MS, 253 | FD_PERIOD_MS); 254 | timer_started = true; 255 | 256 | create_memory_meters(r); 257 | create_gc_timers(r); 258 | beforeStats = alloc_heap_stats(); 259 | 260 | Nan::AddGCPrologueCallback(beforeGC); 261 | Nan::AddGCEpilogueCallback(afterGC); 262 | 263 | open_fd_gauge = r->gauge(node_id("openFileDescriptorsCount")); 264 | max_fd_gauge = r->gauge(node_id("maxFileDescriptorsCount")); 265 | } 266 | } 267 | 268 | auto maybe_dev_mode = options->Get(context, devModeKey); 269 | if (!maybe_dev_mode.IsEmpty()) { 270 | dev_mode = maybe_dev_mode.ToLocalChecked().As()->Value(); 271 | } 272 | } 273 | 274 | if (!log_dirs.empty()) { 275 | atlas_client().SetLoggingDirs(log_dirs); 276 | } 277 | atlas_client().Start(); 278 | started = true; 279 | } 280 | 281 | NAN_METHOD(stop) { 282 | if (started) { 283 | atlas_client().Stop(); 284 | started = false; 285 | } 286 | 287 | if (timer_started) { 288 | uv_timer_stop(&lag_timer); 289 | uv_timer_stop(&fd_timer); 290 | prev_timestamp = 0; 291 | 292 | Nan::RemoveGCPrologueCallback(beforeGC); 293 | Nan::RemoveGCEpilogueCallback(afterGC); 294 | free_heap_stats(beforeStats); 295 | beforeStats = nullptr; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const atlas = require('../'); 4 | const chai = require('chai'); 5 | const assert = chai.assert; 6 | const expect = chai.expect; 7 | 8 | describe('atlas extension', () => { 9 | it('should provide counters', () => { 10 | let counter = atlas.counter('foo'); 11 | assert.equal(counter.increment(), undefined); 12 | assert.equal(counter.count(), 1); 13 | assert.equal(counter.add(3), undefined); 14 | assert.equal(counter.count(), 4); 15 | 16 | let counter3 = atlas.counter('foo', { 17 | k: 'v2' 18 | }); 19 | assert.equal(counter3.increment(), undefined); 20 | assert.equal(counter3.count(), 1); 21 | }); 22 | 23 | it('should handle counter.increment(number)', () => { 24 | let counter = atlas.counter('incr_num'); 25 | counter.increment(2); 26 | assert.equal(counter.count(), 2); 27 | }); 28 | 29 | it('should provide double counters', () => { 30 | let counter = atlas.dcounter('fooDouble'); 31 | assert.equal(counter.increment(), undefined); 32 | assert.equal(counter.count(), 1); 33 | assert.equal(counter.add(0.42), undefined); 34 | assert.equal(counter.count(), 1.42); 35 | }); 36 | 37 | it('should handle fractional increments', () => { 38 | let ic = atlas.counter('int_counter'); 39 | let dc = atlas.dcounter('double_counter'); 40 | let counters = [ ic, dc ]; 41 | 42 | for (let counter of counters) { 43 | counter.increment(0.3); 44 | counter.increment(0.3); 45 | counter.increment(0.3); 46 | counter.increment(0.3); 47 | } 48 | assert.equal(ic.count(), 0); 49 | assert.equal(dc.count(), 1.2); 50 | }); 51 | 52 | const NANOS = 1000 * 1000 * 1000; 53 | it('should provide timers', () => { 54 | let timer = atlas.timer('t'); 55 | // 50 microseconds 56 | assert.equal(timer.record(1, 50 * 1000), undefined); 57 | assert.equal(timer.count(), 1); 58 | assert.equal(timer.totalTime(), 1 + 50000 / NANOS); 59 | assert.equal(timer.record(0, 3 * 1000), undefined); 60 | assert.equal(timer.count(), 2); 61 | assert.equal(timer.totalTime(), 1 + 53000 / NANOS); 62 | }); 63 | 64 | it('should provide a nice timeThis wrapper', () => { 65 | let value = atlas.timer('time.this.example').timeThis(() => { 66 | let j = 0; 67 | 68 | for (let i = 0; i < 1000000; ++i) { 69 | j++; 70 | } 71 | return j; 72 | }); 73 | 74 | assert.equal(value, 1000000); 75 | let timer = atlas.timer('time.this.example'); 76 | assert.equal(timer.count(), 1); 77 | assert(timer.totalTime() > 0, 'Some time was recorded'); 78 | }); 79 | 80 | it('should provide gauges that use the last value set', () => { 81 | let g = atlas.gauge('gauge.example'); 82 | g.update(42); 83 | assert.equal(g.value(), 42); 84 | }); 85 | 86 | it('should maybe have a max gauge (integer)', () => { 87 | let mg = atlas.maxGauge('gauge.max', { 88 | k: 'v' 89 | }); 90 | assert.equal(mg.value(), 0); 91 | 92 | mg.update(2); 93 | assert.equal(mg.value(), 2); 94 | 95 | mg.update(3); 96 | assert.equal(mg.value(), 3); 97 | 98 | mg.update(1); 99 | assert.equal(mg.value(), 3); 100 | }); 101 | 102 | it('should have distribution summaries', () => { 103 | let ds = atlas.distSummary('dist.summary'); 104 | 105 | assert.equal(ds.count(), 0); 106 | assert.equal(ds.totalAmount(), 0); 107 | 108 | ds.record(1000); 109 | 110 | assert.equal(ds.count(), 1); 111 | assert.equal(ds.totalAmount(), 1000); 112 | 113 | ds.record(1001); 114 | assert.equal(ds.count(), 2); 115 | assert.equal(ds.totalAmount(), 2001); 116 | }); 117 | 118 | it('should provide a scope decorator that automatically adds tags', () => { 119 | let s = atlas.scope({ 120 | foo: 'bar' 121 | }); 122 | let ctr = s.counter('scoped.name1', { 123 | key: 'v1' 124 | }); 125 | ctr.increment(); 126 | 127 | assert.equal(atlas.counter( 128 | 'scoped.name1', { 129 | key: 'v1', 130 | foo: 'bar' 131 | }).count(), 1); 132 | }); 133 | 134 | it('should help time async operations', (mochaDone) => { 135 | atlas.timer('timer.async').timeAsync((done) => { 136 | setTimeout(() => { 137 | done(); 138 | let t = atlas.timer('timer.async'); 139 | assert.equal(t.count(), 1); 140 | assert(t.totalTime() > 0); 141 | mochaDone(); 142 | }, 0); 143 | }); 144 | }); 145 | 146 | it('should have long task timers', () => { 147 | let t = atlas.longTaskTimer('long.task.timer'); 148 | let id = t.start(); 149 | assert(id >= 0); 150 | let j = 0; 151 | 152 | for (let i = 0; i < 1000000; ++i) { 153 | j++; 154 | } 155 | assert(j > 0); 156 | assert.equal(t.activeTasks(), 1); 157 | let duration = t.stop(id); 158 | assert(duration > 0); 159 | assert.equal(t.activeTasks(), 0); 160 | }); 161 | 162 | it('should provide an age gauge', (done) => { 163 | atlas.setUpdateInterval(1); 164 | let a = atlas.age('age.gauge'); 165 | setTimeout(() => { 166 | assert(a.value() >= 0.01); // in seconds 167 | done(); 168 | }, 20); 169 | }); 170 | 171 | it('should provide bucket counters', () => { 172 | let bc = atlas.bucketCounter( 173 | 'example.bucket.c', { 174 | function: 'bytes', 175 | value: 1024 176 | }); 177 | bc.record(1000); 178 | let c = atlas.counter('example.bucket.c', { 179 | bucket: '1024_B' 180 | }); 181 | assert.equal(c.count(), 1); 182 | bc.record(1000); 183 | assert.equal(c.count(), 2); 184 | bc.record(212); 185 | let c2 = atlas.counter('example.bucket.c', { 186 | bucket: '0256_B' 187 | }); 188 | assert.equal(c2.count(), 1); 189 | }); 190 | 191 | it('should provide bucket timers', () => { 192 | let bt = atlas.bucketTimer( 193 | 'example.bucket.t', { 194 | function: 'latency', 195 | value: 3, 196 | unit: 's' 197 | }); 198 | bt.record(1, 0); 199 | let t = atlas.timer('example.bucket.t', { 200 | bucket: '1500ms' 201 | }); 202 | assert.equal(t.count(), 1); 203 | bt.record(1, 200000); 204 | assert.equal(t.count(), 2); 205 | bt.record(0, 212 * 1000); 206 | let t2 = atlas.timer('example.bucket.t', { 207 | bucket: '0375ms' 208 | }); 209 | assert.equal(t2.count(), 1); 210 | }); 211 | 212 | it('should provide bucket distribution summaries', () => { 213 | let bc = atlas.bucketDistSummary( 214 | 'example.bucket.d', { 215 | function: 'decimal', 216 | value: 20000 217 | }); 218 | bc.record(15761); 219 | let c = atlas.distSummary('example.bucket.d', { 220 | bucket: '20_k' 221 | }); 222 | assert.equal(c.count(), 1); 223 | bc.record(12030, 0); 224 | assert.equal(c.count(), 2); 225 | bc.record(20001, 0); 226 | let c2 = atlas.distSummary('example.bucket.d', { 227 | bucket: 'large' 228 | }); 229 | assert.equal(c2.count(), 1); 230 | }); 231 | 232 | it('should provide percentile timers', () => { 233 | let samples = 20000; 234 | let d = atlas.percentileTimer('example.perc.t'); 235 | 236 | for (let i = 0; i < samples; ++i) { 237 | d.record(0, i * 1000 * 1000.0); // record in milliseconds 238 | } 239 | assert.equal(d.count(), samples); 240 | 241 | // millis to seconds 242 | assert.equal(d.totalTime(), (samples - 1) * samples / 2 / 1000); 243 | 244 | for (let i = 0; i <= 100; ++i) { 245 | let expected = (samples / 100) * i / 1000.0; 246 | let threshold = 0.15 * expected; 247 | assert.approximately(d.percentile(i), expected, threshold); 248 | } 249 | }); 250 | 251 | it('should provide percentile distribution summaries', () => { 252 | let samples = 20000; 253 | let d = atlas.percentileDistSummary('example.perc.d'); 254 | 255 | for (let i = 0; i < samples; ++i) { 256 | d.record(i); 257 | } 258 | assert.equal(d.count(), samples); 259 | assert.equal(d.totalAmount(), (samples - 1) * samples / 2); 260 | 261 | for (let i = 0; i <= 100; ++i) { 262 | let expected = (samples / 100) * i; 263 | let threshold = 0.15 * expected; 264 | assert.approximately(d.percentile(i), expected, threshold); 265 | } 266 | }); 267 | 268 | it('should provide interval counters', () => { 269 | let d = atlas.intervalCounter('example.counter.d'); 270 | 271 | d.increment(); 272 | d.increment(10); 273 | 274 | assert.equal(d.count(), 11); 275 | }); 276 | 277 | it('should give some debugging info', () => { 278 | let d = atlas.getDebugInfo(); 279 | expect(d).to.have.all.keys('config', 'measurements'); 280 | assert.isObject(d.config); 281 | assert.isArray(d.measurements); 282 | assert.isAtLeast(d.measurements.length, 2); 283 | }); 284 | 285 | it('should validate metrics when in dev mode', () => { 286 | atlas.setDevMode(true); 287 | const noName = () => { 288 | atlas.counter(''); 289 | }; 290 | assert.throw(noName, /empty/); 291 | 292 | const invalidTagKey = () => { 293 | const tags = {}; 294 | tags['a'.repeat(70)] = 'v'; 295 | atlas.timer('foo', tags); 296 | }; 297 | assert.throw(invalidTagKey, /exceeds length/); 298 | 299 | const invalidTagVal = () => { 300 | atlas.timer('foo', {k: 'a'.repeat(160)}); 301 | }; 302 | assert.throw(invalidTagVal, /exceeds length/); 303 | 304 | const tooManyTags = () => { 305 | const tags = {}; 306 | 307 | for (let i = 0; i < 22; ++i) { 308 | tags[i] = 'v'; 309 | } 310 | atlas.counter('f', tags); 311 | }; 312 | assert.throw(tooManyTags, /user tags/); 313 | 314 | const reserved = () => { 315 | atlas.counter('foo', {'atlas.test': 'foo'}); 316 | }; 317 | assert.throw(reserved, /reserved namespace/); 318 | 319 | }); 320 | 321 | it('should not throw when in prod', () => { 322 | atlas.setDevMode(false); 323 | 324 | // empty name 325 | atlas.counter(''); 326 | 327 | // empty key 328 | atlas.counter('foo', { '': undefined }); 329 | 330 | // empty val 331 | atlas.counter('foo', { k: '' }); 332 | 333 | const invalidTagKey = () => { 334 | const tags = {}; 335 | tags['a'.repeat(70)] = 'v'; 336 | atlas.timer('foo', tags); 337 | }; 338 | invalidTagKey(); 339 | 340 | const invalidTagVal = () => { 341 | atlas.timer('foo', {k: 'a'.repeat(160)}); 342 | }; 343 | invalidTagVal(); 344 | 345 | const tooManyTags = () => { 346 | const tags = {}; 347 | 348 | for (let i = 0; i < 22; ++i) { 349 | tags[i] = 'v'; 350 | } 351 | atlas.counter('f', tags); 352 | }; 353 | tooManyTags(); 354 | 355 | const reserved = () => { 356 | atlas.counter('foo', {'atlas.test': 'foo'}); 357 | }; 358 | reserved(); 359 | 360 | }); 361 | 362 | it('should validate metric IDs', () => { 363 | const noName = atlas.validateNameAndTags(''); 364 | assert.equal(noName.length, 1); 365 | assert.match(noName[0].ERROR, /'name'/); // complains about 'name' 366 | 367 | // empty key 368 | const emptyKey = atlas.validateNameAndTags('foo', { '': undefined }); 369 | assert.equal(emptyKey.length, 1); 370 | assert.match(emptyKey[0].ERROR, /empty/i); 371 | 372 | // empty val 373 | const emptyVal = atlas.validateNameAndTags('foo', { k: '' }); 374 | assert.equal(emptyVal.length, 1); 375 | assert.match(emptyVal[0].ERROR, /key 'k' cannot be empty/i); 376 | 377 | { 378 | const key = 'a'.repeat(70); 379 | const tags = {}; 380 | tags[key] = 'v'; 381 | const keyTooLong = atlas.validateNameAndTags('foo', tags); 382 | assert.equal(keyTooLong.length, 1); 383 | assert.match(keyTooLong[0].ERROR, /aaaa' exceeds/i); 384 | } 385 | 386 | const valTooLong = atlas.validateNameAndTags('foo', {k: 'a'.repeat(160)}); 387 | assert.equal(valTooLong.length, 1); 388 | assert.match(valTooLong[0].ERROR, /aaa' for key 'k' exceeds/i); 389 | 390 | { 391 | const tags = {}; 392 | 393 | for (let i = 0; i < 22; ++i) { 394 | tags[i] = 'v'; 395 | } 396 | const tooManyTags = atlas.validateNameAndTags('f', tags); 397 | assert.equal(tooManyTags.length, 1); 398 | // 22 tags + name > 20 399 | assert.match(tooManyTags[0].ERROR, /too many user tags.*23/i); 400 | } 401 | 402 | const reserved = atlas.validateNameAndTags('foo', {'atlas.test': 'foo'}); 403 | assert.equal(reserved.length, 1); 404 | assert.match(reserved[0].ERROR, /reserved namespace/i); 405 | 406 | // warning only 407 | const warnings = atlas.validateNameAndTags('foo ', {'bar#1': 'val%2'}); 408 | assert.equal(warnings.length, 3); 409 | 410 | for (const issue of warnings) { 411 | assert.match(issue.WARN, /encoded as/); 412 | } 413 | }); 414 | }); 415 | -------------------------------------------------------------------------------- /test/metrics-test.txt: -------------------------------------------------------------------------------- 1 | {"type": "TIMER", "name": "job_run_time", "tags":{"test_type": "load", "job_type": "presto", "job_name": "adhoc_0002"}, "value": 9120} 2 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 403} 3 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 7750} 4 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 5 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 173} 6 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 21} 7 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 196} 8 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 9 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 117000} 10 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 100800} 11 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 30095999} 12 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 7750} 13 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 403800} 14 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 15 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 403} 16 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 403} 17 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 9241421345} 18 | {"type": "TIMER", "name": "job_run_time", "tags": {"test_type": "load", "job_type": "presto", "job_name": "adhoc_0002"}, "value": 14115} 19 | {"type": "TIMER", "name": "job_run_time", "tags": {"test_type": "load", "job_type": "presto", "job_name": "adhoc_0003"}, "value": 10142} 20 | {"type": "TIMER", "name": "job_run_time", "tags": {"test_type": "load", "job_type": "presto", "job_name": "adhoc_0003"}, "value": 12650} 21 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 408} 22 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 12730} 23 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 24 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 173} 25 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 23} 26 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 198} 27 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 28 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 120599} 29 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 101400} 30 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 50760000} 31 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 12730} 32 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 385800} 33 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 34 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 408} 35 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 408} 36 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 17821552616} 37 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 403} 38 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 7860} 39 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 40 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 396} 41 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 68} 42 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 8460} 43 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 19} 44 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 45 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 89} 46 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 83} 47 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 48 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 19} 49 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 124800} 50 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 105} 51 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 107400} 52 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 53 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 30816000} 54 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 130200} 55 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 7860} 56 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 109200} 57 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 459000} 58 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 33480000} 59 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 60 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 8460} 61 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 403} 62 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 403} 63 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 468000} 64 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 51341803142} 65 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 66 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 396} 67 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 396} 68 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 52196734650} 69 | {"type": "TIMER", "name": "job_run_time", "tags": {"test_type": "load", "job_type": "presto", "job_name": "adhoc_0009"}, "value": 13138} 70 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 348} 71 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 8530} 72 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 73 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 30} 74 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 0} 75 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 31} 76 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 77 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 111000} 78 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 108000} 79 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 34164000} 80 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 8530} 81 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 389400} 82 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 83 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 348} 84 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 348} 85 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 57038108638} 86 | {"type": "TIMER", "name": "job_run_time", "tags": {"test_type": "load", "job_type": "presto", "job_name": "adhoc_0009"}, "value": 11109} 87 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 406} 88 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 9590} 89 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 1} 90 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 29} 91 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 0} 92 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 31} 93 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 94 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 122400} 95 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 111000} 96 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 38268000} 97 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 9590} 98 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 463800} 99 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 100 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 406} 101 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 406} 102 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 66173282810} 103 | {"type": "TIMER", "name": "job_run_time", "tags":{"test_type": "load", "job_type": "presto", "job_name": "adhoc_0002"}, "value": 9618} 104 | {"type": "TIMER", "name": "job_run_time", "tags":{"test_type": "load", "job_type": "presto", "job_name": "adhoc_0002"}, "value": 14608} 105 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 408} 106 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 8320} 107 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 409} 108 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 109 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 13260} 110 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 111 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 29} 112 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 29} 113 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 0} 114 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 0} 115 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 31} 116 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 117 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 30} 118 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 104400} 119 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 120 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 102000} 121 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 103800} 122 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 32795999} 123 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 101400} 124 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 8320} 125 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 52704000} 126 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 429600} 127 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 128 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 13260} 129 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 465600} 130 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 408} 131 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 132 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 408} 133 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 9664420499} 134 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 409} 135 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 409} 136 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 19041019767} 137 | {"type": "TIMER", "name": "job_run_time", "tags":{"test_type": "load", "job_type": "presto", "job_name": "adhoc_0003"}, "value": 7121} 138 | {"type": "TIMER", "name": "job_run_time", "tags":{"test_type": "load", "job_type": "presto", "job_name": "adhoc_0003"}, "value": 10133} 139 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 405} 140 | {"type": "COUNTER", "name": "completedTasks", "tags": null, "value": 407} 141 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 5560} 142 | {"type": "TIMER", "name": "elapsedTime", "tags": null, "value": 8600} 143 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 144 | {"type": "TIMER", "name": "queuedTime", "tags": null, "value": 0} 145 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 85} 146 | {"type": "TIMER", "name": "analysisTime", "tags": null, "value": 66} 147 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 7} 148 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 94} 149 | {"type": "TIMER", "name": "distributedPlanningTime", "tags": null, "value": 8} 150 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 151 | {"type": "TIMER", "name": "totalPlanningTime", "tags": null, "value": 76} 152 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 111000} 153 | {"type": "TIMER", "name": "finishingTime", "tags": null, "value": 0} 154 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 108600} 155 | {"type": "TIMER", "name": "totalCpuTime", "tags": null, "value": 111600} 156 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 21024000} 157 | {"type": "TIMER", "name": "totalUserTime", "tags": null, "value": 109200} 158 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 5560} 159 | {"type": "TIMER", "name": "totalBlockedTime", "tags": null, "value": 33443999} 160 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 487800} 161 | {"type": "TIMER", "name": "executionTime", "tags": null, "value": 8600} 162 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 163 | {"type": "TIMER", "name": "totalScheduledTime", "tags": null, "value": 481200} 164 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 405} 165 | {"type": "TIMER", "name": "totalDrivers", "tags": null, "value": 4613} 166 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 405} 167 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 26593494238} 168 | {"type": "TIMER", "name": "totalTasks", "tags": null, "value": 407} 169 | {"type": "TIMER", "name": "completedTasks", "tags": null, "value": 407} 170 | {"type": "TIMER", "name": "cumulativeMemory", "tags": null, "value": 55979018329} 171 | -------------------------------------------------------------------------------- /src/functions.cc: -------------------------------------------------------------------------------- 1 | #include "functions.h" 2 | #include "atlas.h" 3 | #include "utils.h" 4 | #include 5 | #include 6 | #include 7 | 8 | using atlas::meter::AnalyzeTags; 9 | using atlas::meter::BucketFunction; 10 | using atlas::meter::IdPtr; 11 | using atlas::meter::Measurement; 12 | using atlas::meter::Measurements; 13 | using atlas::meter::Tags; 14 | using atlas::meter::ValidationIssue; 15 | using atlas::meter::ValidationIssues; 16 | using v8::Context; 17 | using v8::Function; 18 | using v8::FunctionTemplate; 19 | using v8::Local; 20 | using v8::Maybe; 21 | using v8::Object; 22 | 23 | NAN_METHOD(set_dev_mode) { 24 | if (info.Length() == 1 && info[0]->IsBoolean()) { 25 | auto b = Nan::To(info[0]).FromJust(); 26 | dev_mode = b; 27 | } 28 | } 29 | 30 | static void addTags(v8::Isolate* isolate, const v8::Local& object, 31 | Tags* tags) { 32 | auto context = isolate->GetCurrentContext(); 33 | auto maybe_props = object->GetOwnPropertyNames(context); 34 | if (!maybe_props.IsEmpty()) { 35 | auto props = maybe_props.ToLocalChecked(); 36 | auto n = props->Length(); 37 | for (uint32_t i = 0; i < n; ++i) { 38 | const auto& key = props->Get(context, i).ToLocalChecked(); 39 | const auto& value = object->Get(context, key).ToLocalChecked(); 40 | const auto& k = std::string(*Nan::Utf8String(key)); 41 | const auto& v = std::string(*Nan::Utf8String(value)); 42 | tags->add(k.c_str(), v.c_str()); 43 | } 44 | } 45 | } 46 | 47 | NAN_METHOD(validate_name_tags) { 48 | const char* err_msg = nullptr; 49 | std::string name; 50 | ValidationIssues res; 51 | Tags tags; 52 | auto context = Nan::GetCurrentContext(); 53 | auto ret = Nan::New(); 54 | 55 | if (info.Length() > 2) { 56 | err_msg = 57 | "Expecting at most two arguments: a name and an object describing tags"; 58 | goto error; 59 | } 60 | 61 | if (info.Length() >= 1) { 62 | name = *Nan::Utf8String(info[0]); 63 | tags.add("name", name.c_str()); 64 | } 65 | 66 | if (info.Length() == 2 && !(info[1]->IsUndefined() || info[1]->IsNull())) { 67 | const auto& maybe_o = info[1]; 68 | if (maybe_o->IsObject()) { 69 | addTags(info.GetIsolate(), 70 | maybe_o->ToObject(Nan::GetCurrentContext()).ToLocalChecked(), 71 | &tags); 72 | } else { 73 | err_msg = 74 | "Expected an object with string keys and string values as the second " 75 | "argument"; 76 | goto error; 77 | } 78 | } 79 | 80 | res = AnalyzeTags(tags); 81 | if (res.empty()) { 82 | return; 83 | } 84 | 85 | for (const auto& issue : res) { 86 | auto js_issue = Nan::New(); 87 | auto level = 88 | issue.level == ValidationIssue::Level::ERROR ? "ERROR" : "WARN"; 89 | js_issue 90 | ->Set(context, Nan::New(level).ToLocalChecked(), 91 | Nan::New(issue.description.c_str()).ToLocalChecked()) 92 | .FromJust(); 93 | ret->Set(context, ret->Length(), js_issue).FromJust(); 94 | } 95 | error: 96 | if (err_msg != nullptr) { 97 | auto js_issue = Nan::New(); 98 | js_issue 99 | ->Set(context, Nan::New("ERROR").ToLocalChecked(), 100 | Nan::New(err_msg).ToLocalChecked()) 101 | .FromJust(); 102 | ret->Set(context, ret->Length(), js_issue).FromJust(); 103 | } 104 | 105 | info.GetReturnValue().Set(ret); 106 | } 107 | 108 | NAN_METHOD(measurements) { 109 | auto context = Nan::GetCurrentContext(); 110 | auto config = atlas_client().GetConfig(); 111 | auto common_tags = config->CommonTags(); 112 | const auto& measurements = atlas_registry()->measurements(); 113 | 114 | auto ret = Nan::New(); 115 | auto name = Nan::New("name").ToLocalChecked(); 116 | 117 | for (const auto& m : measurements) { 118 | auto measurement = Nan::New(); 119 | auto tags = Nan::New(); 120 | tags->Set(context, name, Nan::New(m.id->Name()).ToLocalChecked()).FromJust(); 121 | 122 | const auto& t = m.id->GetTags(); 123 | for (const auto& kv : common_tags) { 124 | tags->Set(context, Nan::New(kv.first.get()).ToLocalChecked(), 125 | Nan::New(kv.second.get()).ToLocalChecked()) 126 | .FromJust(); 127 | } 128 | for (const auto& kv : t) { 129 | tags->Set(context, Nan::New(kv.first.get()).ToLocalChecked(), 130 | Nan::New(kv.second.get()).ToLocalChecked()) 131 | .FromJust(); 132 | } 133 | measurement->Set(context, Nan::New("tags").ToLocalChecked(), tags).FromJust(); 134 | measurement 135 | ->Set(context, Nan::New("value").ToLocalChecked(), Nan::New(m.value)) 136 | .FromJust(); 137 | ret->Set(context, ret->Length(), measurement).FromJust(); 138 | } 139 | 140 | info.GetReturnValue().Set(ret); 141 | } 142 | 143 | NAN_METHOD(config) { 144 | auto currentCfg = atlas_client().GetConfig(); 145 | const auto& endpoints = currentCfg->EndpointConfiguration(); 146 | const auto& log = currentCfg->LogConfiguration(); 147 | const auto& http = currentCfg->HttpConfiguration(); 148 | auto context = Nan::GetCurrentContext(); 149 | auto ret = Nan::New(); 150 | ret->Set(context, Nan::New("evaluateUrl").ToLocalChecked(), 151 | Nan::New(endpoints.evaluate.c_str()).ToLocalChecked()) 152 | .FromJust(); 153 | ret->Set(context, Nan::New("subscriptionsUrl").ToLocalChecked(), 154 | Nan::New(endpoints.subscriptions.c_str()).ToLocalChecked()) 155 | .FromJust(); 156 | ret->Set(context, Nan::New("publishUrl").ToLocalChecked(), 157 | Nan::New(endpoints.publish.c_str()).ToLocalChecked()) 158 | .FromJust(); 159 | ret->Set(context, Nan::New("subscriptionsRefreshMillis").ToLocalChecked(), 160 | Nan::New(static_cast(currentCfg->SubRefreshMillis()))) 161 | .FromJust(); 162 | ret->Set(context, Nan::New("batchSize").ToLocalChecked(), 163 | Nan::New(http.batch_size)) 164 | .FromJust(); 165 | ret->Set(context, Nan::New("connectTimeout").ToLocalChecked(), 166 | Nan::New(http.connect_timeout)) 167 | .FromJust(); 168 | ret->Set(context, Nan::New("readTimeout").ToLocalChecked(), 169 | Nan::New(http.read_timeout)) 170 | .FromJust(); 171 | ret->Set(context, Nan::New("dumpMetrics").ToLocalChecked(), 172 | Nan::New(log.dump_metrics)) 173 | .FromJust(); 174 | ret->Set(context, Nan::New("dumpSubscriptions").ToLocalChecked(), 175 | Nan::New(log.dump_subscriptions)) 176 | .FromJust(); 177 | ret->Set(context, Nan::New("publishEnabled").ToLocalChecked(), 178 | Nan::New(currentCfg->IsMainEnabled())) 179 | .FromJust(); 180 | 181 | auto pubCfg = Nan::New(); 182 | for (const auto& expr : currentCfg->PublishConfig()) { 183 | pubCfg 184 | ->Set(context, pubCfg->Length(), 185 | Nan::New(expr.c_str()).ToLocalChecked()) 186 | .FromJust(); 187 | } 188 | ret->Set(context, Nan::New("publishConfig").ToLocalChecked(), pubCfg).FromJust(); 189 | auto commonTags = Nan::New(); 190 | for (const auto& kv : currentCfg->CommonTags()) { 191 | commonTags 192 | ->Set(context, Nan::New(kv.first.get()).ToLocalChecked(), 193 | Nan::New(kv.second.get()).ToLocalChecked()) 194 | .FromJust(); 195 | } 196 | ret->Set(context, Nan::New("commonTags").ToLocalChecked(), commonTags) 197 | .FromJust(); 198 | ret->Set(context, Nan::New("loggingDirectory").ToLocalChecked(), 199 | Nan::New(currentCfg->LoggingDirectory().c_str()).ToLocalChecked()) 200 | .FromJust(); 201 | 202 | info.GetReturnValue().Set(ret); 203 | } 204 | 205 | static Measurement getMeasurement(v8::Isolate* isolate, Local v) { 206 | std::string err_msg; 207 | auto context = Nan::GetCurrentContext(); 208 | auto o = v->ToObject(context).ToLocalChecked(); 209 | auto nameVal = 210 | Nan::Get(o, Nan::New("name").ToLocalChecked()).ToLocalChecked(); 211 | Nan::Utf8String utf8(nameVal); 212 | std::string name(*utf8); 213 | auto timestamp = 214 | Nan::To( 215 | Nan::Get(o, Nan::New("timestamp").ToLocalChecked()).ToLocalChecked()) 216 | .FromJust(); 217 | auto value = 218 | Nan::To( 219 | Nan::Get(o, Nan::New("value").ToLocalChecked()).ToLocalChecked()) 220 | .FromJust(); 221 | 222 | auto tagsObj = Nan::Get(o, Nan::New("tags").ToLocalChecked()) 223 | .ToLocalChecked() 224 | ->ToObject(context) 225 | .ToLocalChecked(); 226 | Tags tags; 227 | tagsFromObject(isolate, tagsObj, &tags, &err_msg); 228 | 229 | auto id = atlas_registry()->CreateId(name, tags); 230 | return Measurement{id, timestamp, value}; 231 | } 232 | 233 | NAN_METHOD(push) { 234 | if (info.Length() == 1 && info[0]->IsArray()) { 235 | auto measurements = info[0].As(); 236 | auto context = Nan::GetCurrentContext(); 237 | Measurements ms; 238 | for (uint32_t i = 0; i < measurements->Length(); ++i) { 239 | ms.push_back(getMeasurement( 240 | info.GetIsolate(), measurements->Get(context, i).ToLocalChecked())); 241 | } 242 | atlas_client().Push(ms); 243 | } else { 244 | Nan::ThrowError("atlas.push() expects an array of measurements"); 245 | } 246 | } 247 | 248 | constexpr int kMaxArgs = 3; 249 | 250 | static void CreateConstructor(const Nan::FunctionCallbackInfo& info, 251 | Nan::Persistent& constructor, 252 | const char* name) { 253 | const int argc = info.Length(); 254 | if (argc > kMaxArgs) { 255 | std::ostringstream os; 256 | os << "Too many arguments to " << name << " (" << argc 257 | << " received, max is " << kMaxArgs << ")"; 258 | Nan::ThrowError(os.str().c_str()); 259 | return; 260 | } 261 | if (argc == 0 || info[0]->IsUndefined()) { 262 | Nan::ThrowError("Need at least a name argument"); 263 | return; 264 | } 265 | Local argv[kMaxArgs]; 266 | for (auto i = 0; i < argc; ++i) { 267 | argv[i] = info[i]; 268 | } 269 | 270 | auto cons = Nan::New(constructor); 271 | auto context = Nan::GetCurrentContext(); 272 | Nan::TryCatch tc; 273 | auto newInstance = cons->NewInstance(context, argc, argv); 274 | if (newInstance.IsEmpty()) { 275 | if (tc.HasCaught()) { 276 | tc.ReThrow(); 277 | } else { 278 | Nan::ThrowError("Invalid arguments to constructor. Cannot create meter."); 279 | } 280 | } else { 281 | info.GetReturnValue().Set(newInstance.ToLocalChecked()); 282 | } 283 | } 284 | 285 | NAN_METHOD(counter) { 286 | CreateConstructor(info, JsCounter::constructor, "counter"); 287 | } 288 | 289 | NAN_METHOD(dcounter) { 290 | CreateConstructor(info, JsDCounter::constructor, "dcounter"); 291 | } 292 | 293 | NAN_METHOD(interval_counter) { 294 | CreateConstructor(info, JsIntervalCounter::constructor, "intervalCounter"); 295 | } 296 | 297 | NAN_METHOD(max_gauge) { 298 | CreateConstructor(info, JsMaxGauge::constructor, "maxGauge"); 299 | } 300 | 301 | NAN_METHOD(dist_summary) { 302 | CreateConstructor(info, JsDistSummary::constructor, "distSummary"); 303 | } 304 | 305 | NAN_METHOD(bucket_counter) { 306 | CreateConstructor(info, JsBucketCounter::constructor, "bucketCounter"); 307 | } 308 | 309 | NAN_METHOD(bucket_dist_summary) { 310 | CreateConstructor(info, JsBucketDistSummary::constructor, 311 | "bucketDistributionSummary"); 312 | } 313 | 314 | NAN_METHOD(bucket_timer) { 315 | CreateConstructor(info, JsBucketTimer::constructor, "bucketTimer"); 316 | } 317 | 318 | NAN_METHOD(percentile_timer) { 319 | CreateConstructor(info, JsPercentileTimer::constructor, "percentileTimer"); 320 | } 321 | 322 | NAN_METHOD(percentile_dist_summary) { 323 | CreateConstructor(info, JsPercentileDistSummary::constructor, 324 | "percentileDistSummary"); 325 | } 326 | 327 | NAN_METHOD(timer) { CreateConstructor(info, JsTimer::constructor, "timer"); } 328 | 329 | NAN_METHOD(long_task_timer) { 330 | CreateConstructor(info, JsLongTaskTimer::constructor, "longTaskTimer"); 331 | } 332 | 333 | NAN_METHOD(gauge) { CreateConstructor(info, JsGauge::constructor, "gauge"); } 334 | 335 | Nan::Persistent JsCounter::constructor; 336 | Nan::Persistent JsDCounter::constructor; 337 | Nan::Persistent JsIntervalCounter::constructor; 338 | Nan::Persistent JsTimer::constructor; 339 | Nan::Persistent JsLongTaskTimer::constructor; 340 | Nan::Persistent JsGauge::constructor; 341 | Nan::Persistent JsMaxGauge::constructor; 342 | Nan::Persistent JsDistSummary::constructor; 343 | Nan::Persistent JsBucketCounter::constructor; 344 | Nan::Persistent JsBucketDistSummary::constructor; 345 | Nan::Persistent JsBucketTimer::constructor; 346 | Nan::Persistent JsPercentileTimer::constructor; 347 | Nan::Persistent JsPercentileDistSummary::constructor; 348 | 349 | NAN_MODULE_INIT(JsCounter::Init) { 350 | // Prepare constructor template 351 | Local tpl = Nan::New(New); 352 | tpl->SetClassName(Nan::New("JsCounter").ToLocalChecked()); 353 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 354 | 355 | // Prototype 356 | Nan::SetPrototypeMethod(tpl, "count", Count); 357 | Nan::SetPrototypeMethod(tpl, "add", Add); 358 | Nan::SetPrototypeMethod(tpl, "increment", Increment); 359 | 360 | auto context = Nan::GetCurrentContext(); 361 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 362 | Nan::Set(target, Nan::New("JsCounter").ToLocalChecked(), 363 | tpl->GetFunction(context).ToLocalChecked()); 364 | } 365 | 366 | NAN_METHOD(JsCounter::New) { 367 | if (info.IsConstructCall()) { 368 | // Invoked as constructor: `new JsCounter(...)` 369 | JsCounter* obj = new JsCounter(idFromValue(info, info.Length())); 370 | obj->Wrap(info.This()); 371 | info.GetReturnValue().Set(info.This()); 372 | } else { 373 | Nan::ThrowError("not implemented"); 374 | } 375 | } 376 | 377 | NAN_METHOD(JsCounter::Increment) { 378 | long value = 379 | (long)(info[0]->IsUndefined() 380 | ? 1 381 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 382 | JsCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 383 | ctr->counter_->Add(value); 384 | } 385 | 386 | NAN_METHOD(JsCounter::Add) { 387 | long value = 388 | (long)(info[0]->IsUndefined() 389 | ? 0 390 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 391 | 392 | JsCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 393 | ctr->counter_->Add(value); 394 | } 395 | 396 | NAN_METHOD(JsCounter::Count) { 397 | JsCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 398 | double count = ctr->counter_->Count(); 399 | info.GetReturnValue().Set(count); 400 | } 401 | 402 | JsCounter::JsCounter(IdPtr id) : counter_{atlas_registry()->counter(id)} {} 403 | 404 | NAN_MODULE_INIT(JsDCounter::Init) { 405 | // Prepare constructor template 406 | Local tpl = Nan::New(New); 407 | tpl->SetClassName(Nan::New("JsDCounter").ToLocalChecked()); 408 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 409 | 410 | // Prototype 411 | Nan::SetPrototypeMethod(tpl, "count", Count); 412 | Nan::SetPrototypeMethod(tpl, "add", Add); 413 | Nan::SetPrototypeMethod(tpl, "increment", Increment); 414 | 415 | auto context = Nan::GetCurrentContext(); 416 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 417 | Nan::Set(target, Nan::New("JsDCounter").ToLocalChecked(), 418 | tpl->GetFunction(context).ToLocalChecked()); 419 | } 420 | 421 | NAN_METHOD(JsDCounter::New) { 422 | if (info.IsConstructCall()) { 423 | // Invoked as constructor: `new JsDCounter(...)` 424 | JsDCounter* obj = new JsDCounter(idFromValue(info, info.Length())); 425 | obj->Wrap(info.This()); 426 | info.GetReturnValue().Set(info.This()); 427 | } else { 428 | Nan::ThrowError("not implemented"); 429 | } 430 | } 431 | 432 | NAN_METHOD(JsDCounter::Increment) { 433 | double value = 434 | info[0]->IsUndefined() 435 | ? 1.0 436 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 437 | JsDCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 438 | ctr->counter_->Add(value); 439 | } 440 | 441 | NAN_METHOD(JsDCounter::Add) { 442 | double value = 443 | info[0]->IsUndefined() 444 | ? 0.0 445 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 446 | 447 | JsDCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 448 | ctr->counter_->Add(value); 449 | } 450 | 451 | NAN_METHOD(JsDCounter::Count) { 452 | JsDCounter* ctr = Nan::ObjectWrap::Unwrap(info.This()); 453 | double count = ctr->counter_->Count(); 454 | info.GetReturnValue().Set(count); 455 | } 456 | 457 | JsDCounter::JsDCounter(IdPtr id) : counter_{atlas_registry()->dcounter(id)} {} 458 | 459 | NAN_MODULE_INIT(JsIntervalCounter::Init) { 460 | // Prepare constructor template 461 | Local tpl = Nan::New(New); 462 | tpl->SetClassName(Nan::New("JsIntervalCounter").ToLocalChecked()); 463 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 464 | 465 | // Prototype 466 | Nan::SetPrototypeMethod(tpl, "count", Count); 467 | Nan::SetPrototypeMethod(tpl, "secondsSinceLastUpdate", 468 | SecondsSinceLastUpdate); 469 | Nan::SetPrototypeMethod(tpl, "add", Add); 470 | Nan::SetPrototypeMethod(tpl, "increment", Increment); 471 | 472 | auto context = Nan::GetCurrentContext(); 473 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 474 | Nan::Set(target, Nan::New("JsIntervalCounter").ToLocalChecked(), 475 | tpl->GetFunction(context).ToLocalChecked()); 476 | } 477 | 478 | NAN_METHOD(JsIntervalCounter::New) { 479 | if (info.IsConstructCall()) { 480 | // Invoked as constructor: `new JsIntervalCounter(...)` 481 | JsIntervalCounter* obj = 482 | new JsIntervalCounter(idFromValue(info, info.Length())); 483 | obj->Wrap(info.This()); 484 | info.GetReturnValue().Set(info.This()); 485 | } else { 486 | Nan::ThrowError("not implemented"); 487 | } 488 | } 489 | 490 | NAN_METHOD(JsIntervalCounter::Increment) { 491 | long value = 492 | (long)(info[0]->IsUndefined() 493 | ? 1 494 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 495 | JsIntervalCounter* ctr = 496 | Nan::ObjectWrap::Unwrap(info.This()); 497 | ctr->counter_->Add(value); 498 | } 499 | 500 | NAN_METHOD(JsIntervalCounter::Add) { 501 | long value = 502 | (long)(info[0]->IsUndefined() 503 | ? 0 504 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 505 | 506 | JsIntervalCounter* ctr = 507 | Nan::ObjectWrap::Unwrap(info.This()); 508 | ctr->counter_->Add(value); 509 | } 510 | 511 | NAN_METHOD(JsIntervalCounter::Count) { 512 | JsIntervalCounter* ctr = 513 | Nan::ObjectWrap::Unwrap(info.This()); 514 | double count = ctr->counter_->Count(); 515 | info.GetReturnValue().Set(count); 516 | } 517 | 518 | NAN_METHOD(JsIntervalCounter::SecondsSinceLastUpdate) { 519 | JsIntervalCounter* ctr = 520 | Nan::ObjectWrap::Unwrap(info.This()); 521 | double seconds = ctr->counter_->SecondsSinceLastUpdate(); 522 | info.GetReturnValue().Set(seconds); 523 | } 524 | 525 | JsIntervalCounter::JsIntervalCounter(IdPtr id) 526 | : counter_{std::make_shared(atlas_registry(), 527 | id)} {} 528 | 529 | NAN_MODULE_INIT(JsTimer::Init) { 530 | Nan::HandleScope scope; 531 | 532 | // Prepare constructor template 533 | Local tpl = Nan::New(New); 534 | tpl->SetClassName(Nan::New("JsTimer").ToLocalChecked()); 535 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 536 | 537 | // Prototype 538 | Nan::SetPrototypeMethod(tpl, "count", Count); 539 | Nan::SetPrototypeMethod(tpl, "record", Record); 540 | Nan::SetPrototypeMethod(tpl, "timeThis", TimeThis); 541 | Nan::SetPrototypeMethod(tpl, "totalTime", TotalTime); 542 | 543 | auto context = Nan::GetCurrentContext(); 544 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 545 | Nan::Set(target, Nan::New("JsTimer").ToLocalChecked(), 546 | tpl->GetFunction(context).ToLocalChecked()); 547 | } 548 | 549 | NAN_METHOD(JsTimer::New) { 550 | if (info.IsConstructCall()) { 551 | // Invoked as constructor: `new JsTimer(...)` 552 | JsTimer* obj = new JsTimer(idFromValue(info, info.Length())); 553 | obj->Wrap(info.This()); 554 | info.GetReturnValue().Set(info.This()); 555 | } else { 556 | Nan::ThrowError("not implemented"); 557 | } 558 | } 559 | 560 | constexpr long NANOS = 1000L * 1000L * 1000L; 561 | NAN_METHOD(JsTimer::Record) { 562 | JsTimer* timer = Nan::ObjectWrap::Unwrap(info.This()); 563 | 564 | auto context = Nan::GetCurrentContext(); 565 | int64_t seconds = 0, nanos = 0; 566 | if (info.Length() == 1 && info[0]->IsArray()) { 567 | auto hrtime = info[0].As(); 568 | if (hrtime->Length() == 2) { 569 | seconds = 570 | Nan::To(hrtime->Get(context, 0).ToLocalChecked()).FromJust(); 571 | nanos = 572 | Nan::To(hrtime->Get(context, 1).ToLocalChecked()).FromJust(); 573 | } else { 574 | Nan::ThrowError( 575 | "Expecting an array of two elements: seconds, nanos. See " 576 | "process.hrtime()"); 577 | } 578 | } else { 579 | seconds = (long)(info[0]->IsUndefined() 580 | ? 0 581 | : info[0]->NumberValue(context).FromJust()); 582 | nanos = (long)(info[1]->IsUndefined() 583 | ? 0 584 | : info[1]->NumberValue(context).FromJust()); 585 | } 586 | timer->timer_->Record(std::chrono::nanoseconds(seconds * NANOS + nanos)); 587 | } 588 | 589 | NAN_METHOD(JsTimer::TimeThis) { 590 | JsTimer* timer = Nan::ObjectWrap::Unwrap(info.This()); 591 | 592 | if (info.Length() != 1 || !info[0]->IsFunction()) { 593 | Nan::ThrowError("Expecting a function as the argument to timeThis."); 594 | } else { 595 | auto context = Nan::GetCurrentContext(); 596 | auto function = Nan::To(info[0]).ToLocalChecked(); 597 | 598 | const auto& clock = atlas_registry()->clock(); 599 | auto start = clock.MonotonicTime(); 600 | 601 | auto result = 602 | Nan::Call(function, context->Global(), 0, nullptr).ToLocalChecked(); 603 | 604 | auto elapsed_nanos = clock.MonotonicTime() - start; 605 | timer->timer_->Record(std::chrono::nanoseconds(elapsed_nanos)); 606 | info.GetReturnValue().Set(result); 607 | } 608 | } 609 | 610 | NAN_METHOD(JsTimer::TotalTime) { 611 | JsTimer* tmr = Nan::ObjectWrap::Unwrap(info.This()); 612 | double totalTime = tmr->timer_->TotalTime() / 1e9; 613 | info.GetReturnValue().Set(totalTime); 614 | } 615 | 616 | NAN_METHOD(JsTimer::Count) { 617 | JsTimer* tmr = Nan::ObjectWrap::Unwrap(info.This()); 618 | double count = tmr->timer_->Count(); 619 | info.GetReturnValue().Set(count); 620 | } 621 | 622 | JsTimer::JsTimer(IdPtr id) : timer_{atlas_registry()->timer(id)} {} 623 | 624 | JsLongTaskTimer::JsLongTaskTimer(IdPtr id) 625 | : timer_{atlas_registry()->long_task_timer(id)} {} 626 | 627 | NAN_MODULE_INIT(JsLongTaskTimer::Init) { 628 | Nan::HandleScope scope; 629 | 630 | // Prepare constructor template 631 | Local tpl = Nan::New(New); 632 | tpl->SetClassName(Nan::New("JsLongTaskTimer").ToLocalChecked()); 633 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 634 | 635 | // Prototype 636 | Nan::SetPrototypeMethod(tpl, "start", Start); 637 | Nan::SetPrototypeMethod(tpl, "stop", Stop); 638 | Nan::SetPrototypeMethod(tpl, "duration", Duration); 639 | Nan::SetPrototypeMethod(tpl, "activeTasks", ActiveTasks); 640 | 641 | auto context = Nan::GetCurrentContext(); 642 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 643 | Nan::Set(target, Nan::New("JsLongTaskTimer").ToLocalChecked(), 644 | tpl->GetFunction(context).ToLocalChecked()); 645 | } 646 | 647 | NAN_METHOD(JsLongTaskTimer::New) { 648 | if (info.IsConstructCall()) { 649 | // Invoked as constructor: `new JsTimer(...)` 650 | JsLongTaskTimer* obj = 651 | new JsLongTaskTimer(idFromValue(info, info.Length())); 652 | obj->Wrap(info.This()); 653 | info.GetReturnValue().Set(info.This()); 654 | } else { 655 | Nan::ThrowError("not implemented"); 656 | } 657 | } 658 | 659 | NAN_METHOD(JsLongTaskTimer::Start) { 660 | auto wrapper = Nan::ObjectWrap::Unwrap(info.This()); 661 | auto start = (double)wrapper->timer_->Start(); 662 | info.GetReturnValue().Set(start); 663 | } 664 | 665 | NAN_METHOD(JsLongTaskTimer::Stop) { 666 | auto wrapper = Nan::ObjectWrap::Unwrap(info.This()); 667 | auto id = (int64_t)( 668 | info[0]->IsUndefined() 669 | ? 0 670 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 671 | auto duration = (double)wrapper->timer_->Stop(id); 672 | info.GetReturnValue().Set(duration); 673 | } 674 | 675 | NAN_METHOD(JsLongTaskTimer::Duration) { 676 | auto wrapper = Nan::ObjectWrap::Unwrap(info.This()); 677 | auto& tmr = wrapper->timer_; 678 | double duration; 679 | if (info.Length() > 0) { 680 | auto id = (int64_t)( 681 | info[0]->IsUndefined() 682 | ? 0 683 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 684 | duration = (double)tmr->Duration(id); 685 | } else { 686 | duration = (double)tmr->Duration(); 687 | } 688 | info.GetReturnValue().Set(duration); 689 | } 690 | 691 | NAN_METHOD(JsLongTaskTimer::ActiveTasks) { 692 | auto wrapper = Nan::ObjectWrap::Unwrap(info.This()); 693 | auto tasks = wrapper->timer_->ActiveTasks(); 694 | info.GetReturnValue().Set(tasks); 695 | } 696 | 697 | NAN_MODULE_INIT(JsGauge::Init) { 698 | Nan::HandleScope scope; 699 | 700 | // Prepare constructor template 701 | Local tpl = Nan::New(New); 702 | tpl->SetClassName(Nan::New("JsGauge").ToLocalChecked()); 703 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 704 | 705 | // Prototype 706 | Nan::SetPrototypeMethod(tpl, "value", Value); 707 | Nan::SetPrototypeMethod(tpl, "update", Update); 708 | 709 | auto context = Nan::GetCurrentContext(); 710 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 711 | Nan::Set(target, Nan::New("JsGauge").ToLocalChecked(), 712 | tpl->GetFunction(context).ToLocalChecked()); 713 | } 714 | 715 | NAN_METHOD(JsGauge::New) { 716 | using namespace v8; 717 | 718 | if (info.IsConstructCall()) { 719 | // Invoked as constructor: `new JsGauge(...)` 720 | JsGauge* obj = new JsGauge(idFromValue(info, info.Length())); 721 | obj->Wrap(info.This()); 722 | info.GetReturnValue().Set(info.This()); 723 | } else { 724 | Nan::ThrowError("not implemented"); 725 | } 726 | } 727 | 728 | NAN_METHOD(JsGauge::Update) { 729 | JsGauge* g = Nan::ObjectWrap::Unwrap(info.This()); 730 | auto value = info[0]->IsUndefined() 731 | ? 0 732 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 733 | g->gauge_->Update(value); 734 | } 735 | 736 | NAN_METHOD(JsGauge::Value) { 737 | JsGauge* g = Nan::ObjectWrap::Unwrap(info.This()); 738 | auto value = g->gauge_->Value(); 739 | info.GetReturnValue().Set(value); 740 | } 741 | 742 | JsGauge::JsGauge(IdPtr id) : gauge_{atlas_registry()->gauge(id)} {} 743 | 744 | NAN_MODULE_INIT(JsMaxGauge::Init) { 745 | Nan::HandleScope scope; 746 | 747 | // Prepare constructor template 748 | Local tpl = Nan::New(New); 749 | tpl->SetClassName(Nan::New("JsMaxGauge").ToLocalChecked()); 750 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 751 | 752 | // Prototype 753 | Nan::SetPrototypeMethod(tpl, "value", Value); 754 | Nan::SetPrototypeMethod(tpl, "update", Update); 755 | 756 | auto context = Nan::GetCurrentContext(); 757 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 758 | Nan::Set(target, Nan::New("JsMaxGauge").ToLocalChecked(), 759 | tpl->GetFunction(context).ToLocalChecked()); 760 | } 761 | 762 | NAN_METHOD(JsMaxGauge::New) { 763 | const auto argc = info.Length(); 764 | if (info.IsConstructCall()) { 765 | // Invoked as constructor: `new JsMaxGauge(...)` 766 | JsMaxGauge* obj = new JsMaxGauge(idFromValue(info, argc)); 767 | obj->Wrap(info.This()); 768 | info.GetReturnValue().Set(info.This()); 769 | } else { 770 | Nan::ThrowError("not implemented"); 771 | } 772 | } 773 | 774 | NAN_METHOD(JsMaxGauge::Value) { 775 | JsMaxGauge* g = Nan::ObjectWrap::Unwrap(info.This()); 776 | auto value = g->max_gauge_->Value(); 777 | info.GetReturnValue().Set(value); 778 | } 779 | 780 | NAN_METHOD(JsMaxGauge::Update) { 781 | JsMaxGauge* g = Nan::ObjectWrap::Unwrap(info.This()); 782 | auto value = info[0]->IsUndefined() 783 | ? 0.0 784 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 785 | g->max_gauge_->Update(value); 786 | } 787 | 788 | JsMaxGauge::JsMaxGauge(IdPtr id) 789 | : max_gauge_{atlas_registry()->max_gauge(id)} {} 790 | 791 | NAN_MODULE_INIT(JsDistSummary::Init) { 792 | Nan::HandleScope scope; 793 | 794 | // Prepare constructor template 795 | Local tpl = Nan::New(New); 796 | tpl->SetClassName(Nan::New("JsDistSummary").ToLocalChecked()); 797 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 798 | 799 | // Prototype 800 | Nan::SetPrototypeMethod(tpl, "totalAmount", TotalAmount); 801 | Nan::SetPrototypeMethod(tpl, "count", Count); 802 | Nan::SetPrototypeMethod(tpl, "record", Record); 803 | 804 | auto context = Nan::GetCurrentContext(); 805 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 806 | Nan::Set(target, Nan::New("JsDistSummary").ToLocalChecked(), 807 | tpl->GetFunction(context).ToLocalChecked()); 808 | } 809 | 810 | NAN_METHOD(JsDistSummary::New) { 811 | const auto argc = info.Length(); 812 | if (info.IsConstructCall()) { 813 | // Invoked as constructor: `new JsDistSummary(...)` 814 | JsDistSummary* obj = new JsDistSummary(idFromValue(info, argc)); 815 | obj->Wrap(info.This()); 816 | info.GetReturnValue().Set(info.This()); 817 | } else { 818 | Nan::ThrowError("not implemented"); 819 | } 820 | } 821 | 822 | NAN_METHOD(JsDistSummary::Count) { 823 | JsDistSummary* g = Nan::ObjectWrap::Unwrap(info.This()); 824 | auto value = (double)(g->dist_summary_->Count()); 825 | info.GetReturnValue().Set(value); 826 | } 827 | 828 | NAN_METHOD(JsDistSummary::TotalAmount) { 829 | JsDistSummary* g = Nan::ObjectWrap::Unwrap(info.This()); 830 | auto value = (double)(g->dist_summary_->TotalAmount()); 831 | info.GetReturnValue().Set(value); 832 | } 833 | 834 | NAN_METHOD(JsDistSummary::Record) { 835 | JsDistSummary* g = Nan::ObjectWrap::Unwrap(info.This()); 836 | auto value = static_cast( 837 | info[0]->IsUndefined() 838 | ? 0 839 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 840 | g->dist_summary_->Record(value); 841 | } 842 | 843 | JsDistSummary::JsDistSummary(IdPtr id) 844 | : dist_summary_{atlas_registry()->distribution_summary(id)} {} 845 | 846 | static std::string kEmptyString; 847 | static int64_t GetNumKey(Local context, Local object, 848 | const char* key) { 849 | const auto& lookup_key = Nan::New(key).ToLocalChecked(); 850 | auto maybeResult = object->Get(context, lookup_key); 851 | if (maybeResult.IsEmpty()) { 852 | return -1; 853 | } 854 | 855 | auto val = maybeResult.ToLocalChecked() 856 | ->NumberValue(Nan::GetCurrentContext()) 857 | .FromJust(); 858 | return static_cast(val); 859 | } 860 | 861 | static std::string GetStrKey(Local context, Local object, 862 | const char* key) { 863 | const auto& lookup_key = Nan::New(key).ToLocalChecked(); 864 | auto maybeResult = object->Get(context, lookup_key); 865 | if (maybeResult.IsEmpty()) { 866 | return kEmptyString; 867 | } 868 | 869 | Nan::Utf8String val{maybeResult.ToLocalChecked()}; 870 | return std::string{*val}; 871 | } 872 | 873 | static std::chrono::nanoseconds GetDuration(int64_t value, 874 | const std::string& unit) { 875 | if (unit == "ns") { 876 | return std::chrono::nanoseconds(value); 877 | } 878 | if (unit == "us") { 879 | return std::chrono::microseconds(value); 880 | } 881 | if (unit == "ms") { 882 | return std::chrono::milliseconds(value); 883 | } 884 | if (unit == "s") { 885 | return std::chrono::seconds(value); 886 | } 887 | if (unit == "min") { 888 | return std::chrono::minutes(value); 889 | } 890 | if (unit == "h") { 891 | return std::chrono::hours(value); 892 | } 893 | 894 | return std::chrono::nanoseconds::zero(); 895 | } 896 | 897 | // { "function": "age", "value": 1, "unit": "s" } 898 | // { "function": "ageBiasOld", "value": 1, "unit": "min" } 899 | // { "function": "latency", "value": 500, "unit": "ms" } 900 | // { "function": "latencyBiasOld", "value": 500, "unit": "us" } 901 | // { "function": "bytes", "value": 1024 * 1024} 902 | // { "function": "decimal", "value": 20000 } 903 | // 904 | static Maybe bucketFuncFromObject(v8::Isolate* isolate, 905 | Local object) { 906 | const char* kUsageError = 907 | "Bucket Function Generator expects an object with a key 'function'" 908 | ", a key 'value', and an optional key 'unit' (for generators that take a " 909 | "time unit)"; 910 | 911 | using atlas::meter::bucket_functions::Age; 912 | using atlas::meter::bucket_functions::AgeBiasOld; 913 | using atlas::meter::bucket_functions::Bytes; 914 | using atlas::meter::bucket_functions::Decimal; 915 | using atlas::meter::bucket_functions::Latency; 916 | using atlas::meter::bucket_functions::LatencyBiasSlow; 917 | 918 | auto context = isolate->GetCurrentContext(); 919 | auto props = object->GetOwnPropertyNames(context).ToLocalChecked(); 920 | if (props->Length() < 2 || props->Length() > 3) { 921 | Nan::ThrowError(kUsageError); 922 | } 923 | 924 | const auto& function = GetStrKey(context, object, "function"); 925 | if (function.empty()) { 926 | Nan::ThrowError(kUsageError); 927 | return v8::Nothing(); 928 | } 929 | 930 | const auto value = GetNumKey(context, object, "value"); 931 | if (value < 0) { 932 | Nan::ThrowError(kUsageError); 933 | return v8::Nothing(); 934 | } 935 | 936 | if (function == "bytes") { 937 | return v8::Just(Bytes(value)); 938 | } 939 | if (function == "decimal") { 940 | return v8::Just(Decimal(value)); 941 | } 942 | // all others require a duration 943 | const auto& unit = GetStrKey(context, object, "unit"); 944 | const auto& duration = GetDuration(value, unit); 945 | if (duration == std::chrono::nanoseconds::zero()) { 946 | std::ostringstream os; 947 | os << "Invalid duration specified: unit=" << unit << " value=" << value 948 | << ", valid units are: ns, us, ms, s, min, h"; 949 | Nan::ThrowError(os.str().c_str()); 950 | } 951 | 952 | if (function == "age") { 953 | return v8::Just(Age(duration)); 954 | } 955 | if (function == "ageBiasOld") { 956 | return v8::Just(AgeBiasOld(duration)); 957 | } 958 | if (function == "latency") { 959 | return v8::Just(Latency(duration)); 960 | } 961 | if (function == "latencyBiasSlow") { 962 | return v8::Just(LatencyBiasSlow(duration)); 963 | } 964 | 965 | std::ostringstream os; 966 | os << "Unknown bucket generator function: " << function 967 | << ": valid options are: age, ageBiasOld, latency, latencyBiasSlow, " 968 | "bytes, decimal"; 969 | Nan::ThrowError(os.str().c_str()); 970 | return v8::Nothing(); 971 | } 972 | 973 | NAN_MODULE_INIT(JsBucketCounter::Init) { 974 | Nan::HandleScope scope; 975 | 976 | // Prepare constructor template 977 | Local tpl = Nan::New(New); 978 | tpl->SetClassName(Nan::New("JsBucketCounter").ToLocalChecked()); 979 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 980 | 981 | // Prototype 982 | Nan::SetPrototypeMethod(tpl, "record", Record); 983 | 984 | auto context = Nan::GetCurrentContext(); 985 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 986 | Nan::Set(target, Nan::New("JsBucketCounter").ToLocalChecked(), 987 | tpl->GetFunction(context).ToLocalChecked()); 988 | } 989 | 990 | NAN_METHOD(JsBucketCounter::New) { 991 | const auto argc = info.Length(); 992 | if (argc < 2) { 993 | Nan::ThrowError( 994 | "Need at least two arguments: a name, and an object describing bucket " 995 | "generating function"); 996 | return; 997 | } 998 | 999 | if (info.IsConstructCall()) { 1000 | // Invoked as constructor: `new JsBucketCounter(...)` 1001 | auto context = Nan::GetCurrentContext(); 1002 | const auto& bucket_function = bucketFuncFromObject( 1003 | info.GetIsolate(), info[argc - 1]->ToObject(context).ToLocalChecked()); 1004 | auto obj = new JsBucketCounter(idFromValue(info, argc - 1), 1005 | bucket_function.FromJust()); 1006 | obj->Wrap(info.This()); 1007 | info.GetReturnValue().Set(info.This()); 1008 | } else { 1009 | Nan::ThrowError("not implemented"); 1010 | } 1011 | } 1012 | 1013 | NAN_METHOD(JsBucketCounter::Record) { 1014 | auto g = Nan::ObjectWrap::Unwrap(info.This()); 1015 | auto value = (uint64_t)( 1016 | info[0]->IsUndefined() 1017 | ? 0 1018 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 1019 | g->bucket_counter_->Record(value); 1020 | } 1021 | 1022 | JsBucketCounter::JsBucketCounter(IdPtr id, BucketFunction bucket_function) 1023 | : bucket_counter_{std::make_shared( 1024 | atlas_registry(), id, bucket_function)} {} 1025 | 1026 | NAN_MODULE_INIT(JsBucketDistSummary::Init) { 1027 | Nan::HandleScope scope; 1028 | 1029 | // Prepare constructor template 1030 | Local tpl = Nan::New(New); 1031 | tpl->SetClassName(Nan::New("JsBucketDistSummary").ToLocalChecked()); 1032 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 1033 | 1034 | // Prototype 1035 | Nan::SetPrototypeMethod(tpl, "record", Record); 1036 | 1037 | auto context = Nan::GetCurrentContext(); 1038 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 1039 | Nan::Set(target, Nan::New("JsBucketDistSummary").ToLocalChecked(), 1040 | tpl->GetFunction(context).ToLocalChecked()); 1041 | } 1042 | 1043 | NAN_METHOD(JsBucketDistSummary::New) { 1044 | const auto argc = info.Length(); 1045 | if (argc < 2) { 1046 | Nan::ThrowError( 1047 | "Need at least two arguments: a name, and an object describing bucket " 1048 | "generating function"); 1049 | return; 1050 | } 1051 | 1052 | if (info.IsConstructCall()) { 1053 | // Invoked as constructor: `new JsBucketDistSummary(...)` 1054 | auto context = Nan::GetCurrentContext(); 1055 | const auto& bucket_function = bucketFuncFromObject( 1056 | info.GetIsolate(), info[argc - 1]->ToObject(context).ToLocalChecked()); 1057 | auto obj = new JsBucketDistSummary(idFromValue(info, argc - 1), 1058 | bucket_function.FromJust()); 1059 | obj->Wrap(info.This()); 1060 | info.GetReturnValue().Set(info.This()); 1061 | } else { 1062 | Nan::ThrowError("not implemented"); 1063 | } 1064 | } 1065 | 1066 | NAN_METHOD(JsBucketDistSummary::Record) { 1067 | auto g = Nan::ObjectWrap::Unwrap(info.This()); 1068 | auto value = (uint64_t)( 1069 | info[0]->IsUndefined() 1070 | ? 0 1071 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 1072 | g->bucket_dist_summary_->Record(value); 1073 | } 1074 | 1075 | JsBucketDistSummary::JsBucketDistSummary(IdPtr id, 1076 | BucketFunction bucket_function) 1077 | : bucket_dist_summary_{ 1078 | std::make_shared( 1079 | atlas_registry(), id, bucket_function)} {} 1080 | 1081 | NAN_MODULE_INIT(JsBucketTimer::Init) { 1082 | Nan::HandleScope scope; 1083 | 1084 | // Prepare constructor template 1085 | Local tpl = Nan::New(New); 1086 | tpl->SetClassName(Nan::New("JsBucketTimer").ToLocalChecked()); 1087 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 1088 | 1089 | // Prototype 1090 | Nan::SetPrototypeMethod(tpl, "record", Record); 1091 | 1092 | auto context = Nan::GetCurrentContext(); 1093 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 1094 | Nan::Set(target, Nan::New("JsBucketTimer").ToLocalChecked(), 1095 | tpl->GetFunction(context).ToLocalChecked()); 1096 | } 1097 | 1098 | NAN_METHOD(JsBucketTimer::New) { 1099 | const auto argc = info.Length(); 1100 | if (argc < 2) { 1101 | Nan::ThrowError( 1102 | "Need at least two arguments: a name, and an object describing bucket " 1103 | "generating function"); 1104 | return; 1105 | } 1106 | 1107 | if (info.IsConstructCall()) { 1108 | // Invoked as constructor: `new JsBucketTimer(...)` 1109 | auto context = Nan::GetCurrentContext(); 1110 | const auto& bucket_function = bucketFuncFromObject( 1111 | info.GetIsolate(), info[argc - 1]->ToObject(context).ToLocalChecked()); 1112 | auto obj = new JsBucketTimer(idFromValue(info, argc - 1), 1113 | bucket_function.FromJust()); 1114 | obj->Wrap(info.This()); 1115 | info.GetReturnValue().Set(info.This()); 1116 | } else { 1117 | Nan::ThrowError("not implemented"); 1118 | } 1119 | } 1120 | 1121 | NAN_METHOD(JsBucketTimer::Record) { 1122 | auto g = Nan::ObjectWrap::Unwrap(info.This()); 1123 | auto context = Nan::GetCurrentContext(); 1124 | 1125 | int64_t seconds = 0, nanos = 0; 1126 | if (info.Length() == 1 && info[0]->IsArray()) { 1127 | auto hrtime = info[0].As(); 1128 | if (hrtime->Length() == 2) { 1129 | seconds = 1130 | Nan::To(hrtime->Get(context, 0).ToLocalChecked()).FromJust(); 1131 | nanos = 1132 | Nan::To(hrtime->Get(context, 1).ToLocalChecked()).FromJust(); 1133 | } else { 1134 | Nan::ThrowError( 1135 | "Expecting an array of two elements: seconds, nanos. See " 1136 | "process.hrtime()"); 1137 | } 1138 | } else { 1139 | seconds = (long)(info[0]->IsUndefined() 1140 | ? 0 1141 | : info[0]->NumberValue(context).FromJust()); 1142 | nanos = (long)(info[1]->IsUndefined() 1143 | ? 0 1144 | : info[1]->NumberValue(context).FromJust()); 1145 | } 1146 | g->bucket_timer_->Record(std::chrono::nanoseconds(seconds * NANOS + nanos)); 1147 | } 1148 | 1149 | JsBucketTimer::JsBucketTimer(IdPtr id, BucketFunction bucket_function) 1150 | : bucket_timer_{std::make_shared( 1151 | atlas_registry(), id, bucket_function)} {} 1152 | 1153 | NAN_MODULE_INIT(JsPercentileTimer::Init) { 1154 | Nan::HandleScope scope; 1155 | 1156 | // Prepare constructor template 1157 | Local tpl = Nan::New(New); 1158 | tpl->SetClassName(Nan::New("JsPercentileTimer").ToLocalChecked()); 1159 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 1160 | 1161 | // Prototype 1162 | Nan::SetPrototypeMethod(tpl, "record", Record); 1163 | Nan::SetPrototypeMethod(tpl, "percentile", Percentile); 1164 | Nan::SetPrototypeMethod(tpl, "count", Count); 1165 | Nan::SetPrototypeMethod(tpl, "totalTime", TotalTime); 1166 | 1167 | auto context = Nan::GetCurrentContext(); 1168 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 1169 | Nan::Set(target, Nan::New("JsPercentileTimer").ToLocalChecked(), 1170 | tpl->GetFunction(context).ToLocalChecked()); 1171 | } 1172 | 1173 | NAN_METHOD(JsPercentileTimer::New) { 1174 | if (info.IsConstructCall()) { 1175 | // Invoked as constructor: `new JsPercentileTimer(...)` 1176 | auto obj = new JsPercentileTimer(idFromValue(info, info.Length())); 1177 | obj->Wrap(info.This()); 1178 | info.GetReturnValue().Set(info.This()); 1179 | } else { 1180 | Nan::ThrowError("not implemented"); 1181 | } 1182 | } 1183 | 1184 | NAN_METHOD(JsPercentileTimer::Record) { 1185 | auto t = Nan::ObjectWrap::Unwrap(info.This()); 1186 | auto context = Nan::GetCurrentContext(); 1187 | int64_t seconds = 0, nanos = 0; 1188 | if (info.Length() == 1 && info[0]->IsArray()) { 1189 | auto hrtime = info[0].As(); 1190 | if (hrtime->Length() == 2) { 1191 | seconds = 1192 | Nan::To(hrtime->Get(context, 0).ToLocalChecked()).FromJust(); 1193 | nanos = 1194 | Nan::To(hrtime->Get(context, 1).ToLocalChecked()).FromJust(); 1195 | } else { 1196 | Nan::ThrowError( 1197 | "Expecting an array of two elements: seconds, nanos. See " 1198 | "process.hrtime()"); 1199 | } 1200 | } else if (info.Length() == 2) { 1201 | seconds = (long)(info[0]->IsUndefined() 1202 | ? 0 1203 | : info[0]->NumberValue(context).FromJust()); 1204 | nanos = (long)(info[1]->IsUndefined() 1205 | ? 0 1206 | : info[1]->NumberValue(context).FromJust()); 1207 | } else { 1208 | Nan::ThrowError("Expecting two numbers: seconds and nanoseconds"); 1209 | } 1210 | 1211 | t->perc_timer_->Record(std::chrono::nanoseconds(seconds * NANOS + nanos)); 1212 | } 1213 | 1214 | NAN_METHOD(JsPercentileTimer::TotalTime) { 1215 | auto tmr = Nan::ObjectWrap::Unwrap(info.This()); 1216 | double totalTime = tmr->perc_timer_->TotalTime() / 1e9; 1217 | info.GetReturnValue().Set(totalTime); 1218 | } 1219 | 1220 | NAN_METHOD(JsPercentileTimer::Count) { 1221 | auto tmr = Nan::ObjectWrap::Unwrap(info.This()); 1222 | double count = tmr->perc_timer_->Count(); 1223 | info.GetReturnValue().Set(count); 1224 | } 1225 | 1226 | NAN_METHOD(JsPercentileTimer::Percentile) { 1227 | auto t = Nan::ObjectWrap::Unwrap(info.This()); 1228 | if (info.Length() == 0) { 1229 | Nan::ThrowError( 1230 | "Need the percentile to compute as a number from 0.0 to 100.0"); 1231 | return; 1232 | } 1233 | auto n = info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 1234 | auto value = t->perc_timer_->Percentile(n); 1235 | info.GetReturnValue().Set(value); 1236 | } 1237 | 1238 | JsPercentileTimer::JsPercentileTimer(IdPtr id) 1239 | : perc_timer_{std::make_shared( 1240 | atlas_registry(), id)} {} 1241 | 1242 | NAN_MODULE_INIT(JsPercentileDistSummary::Init) { 1243 | Nan::HandleScope scope; 1244 | 1245 | // Prepare constructor template 1246 | Local tpl = Nan::New(New); 1247 | tpl->SetClassName(Nan::New("JsPercentileDistSummary").ToLocalChecked()); 1248 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 1249 | 1250 | // Prototype 1251 | Nan::SetPrototypeMethod(tpl, "record", Record); 1252 | Nan::SetPrototypeMethod(tpl, "percentile", Percentile); 1253 | Nan::SetPrototypeMethod(tpl, "count", Count); 1254 | Nan::SetPrototypeMethod(tpl, "totalAmount", TotalAmount); 1255 | 1256 | auto context = Nan::GetCurrentContext(); 1257 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 1258 | Nan::Set(target, Nan::New("JsPercentileDistSummary").ToLocalChecked(), 1259 | tpl->GetFunction(context).ToLocalChecked()); 1260 | } 1261 | 1262 | NAN_METHOD(JsPercentileDistSummary::New) { 1263 | if (info.IsConstructCall()) { 1264 | // Invoked as constructor: `new JsPercentileDistSummary(...)` 1265 | auto obj = new JsPercentileDistSummary(idFromValue(info, info.Length())); 1266 | obj->Wrap(info.This()); 1267 | info.GetReturnValue().Set(info.This()); 1268 | } else { 1269 | Nan::ThrowError("not implemented"); 1270 | } 1271 | } 1272 | 1273 | NAN_METHOD(JsPercentileDistSummary::Record) { 1274 | auto t = Nan::ObjectWrap::Unwrap(info.This()); 1275 | auto value = static_cast( 1276 | info[0]->IsUndefined() 1277 | ? 0 1278 | : info[0]->NumberValue(Nan::GetCurrentContext()).FromJust()); 1279 | t->perc_dist_summary_->Record(value); 1280 | } 1281 | 1282 | NAN_METHOD(JsPercentileDistSummary::Count) { 1283 | auto d = Nan::ObjectWrap::Unwrap(info.This()); 1284 | auto value = static_cast(d->perc_dist_summary_->Count()); 1285 | info.GetReturnValue().Set(value); 1286 | } 1287 | 1288 | NAN_METHOD(JsPercentileDistSummary::TotalAmount) { 1289 | auto d = Nan::ObjectWrap::Unwrap(info.This()); 1290 | auto value = static_cast(d->perc_dist_summary_->TotalAmount()); 1291 | info.GetReturnValue().Set(value); 1292 | } 1293 | 1294 | NAN_METHOD(JsPercentileDistSummary::Percentile) { 1295 | auto d = Nan::ObjectWrap::Unwrap(info.This()); 1296 | if (info.Length() == 0) { 1297 | Nan::ThrowError( 1298 | "Need the percentile to compute as a number from 0.0 to 100.0"); 1299 | return; 1300 | } 1301 | auto n = info[0]->NumberValue(Nan::GetCurrentContext()).FromJust(); 1302 | auto value = d->perc_dist_summary_->Percentile(n); 1303 | info.GetReturnValue().Set(value); 1304 | } 1305 | 1306 | JsPercentileDistSummary::JsPercentileDistSummary(IdPtr id) 1307 | : perc_dist_summary_{ 1308 | std::make_shared( 1309 | atlas_registry(), id)} {} 1310 | --------------------------------------------------------------------------------