diff --git a/.circleci/config.yml b/.circleci/config.yml index e30f9cf5149..45e119ccf51 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,6 +22,9 @@ node-core-base: &node-core-base paths: - ./node_modules - ./yarn.lock + - run: + name: Compile native code + command: yarn rebuild - run: name: Unit tests command: yarn test:core diff --git a/.gitignore b/.gitignore index 0e656d44d27..a30159c01e5 100644 --- a/.gitignore +++ b/.gitignore @@ -91,10 +91,6 @@ typings/ ### VisualStudioCode ### .vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json .history @@ -104,5 +100,6 @@ package-lock.json yarn.lock out versions +build docs/test.js !test/node_modules diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 3b3b8e0e980..884913fec60 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,6 +1,7 @@ Component,Origin,License,Copyright require,@types/node,MIT,Copyright Authors require,async-hook-jl,MIT,Copyright 2015 Andreas Madsen +require,hot-shots,MIT,Copyright 2011 Steve Ivy require,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki require,koalas,MIT,Copyright 2013-2017 Brian Woodward require,lodash.kebabcase,MIT,Copyright JS Foundation and other contributors @@ -12,6 +13,7 @@ require,lodash.uniq,MIT,Copyright JS Foundation and other contributors require,methods,MIT,Copyright 2013-2014 TJ Holowaychuk 2013-2014 TJ Holowaychuk require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen require,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki +require,nan,MIT,Copyright 2018 NAN contributors require,opentracing,MIT,Copyright 2016 Resonance Labs Inc require,parent-module,MIT,Copyright Sindre Sorhus require,path-to-regexp,MIT,Copyright 2014 Blake Embrey diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 71e6861cf2a..260f9e25ead 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,8 +2,14 @@ pool: vmImage: vs2017-win2016 steps: -- script: yarn versions && yarn install +- bash: yarn versions + displayName: Versions + +- bash: yarn install displayName: Install dependencies +- bash: yarn rebuild + displayName: Compile native code + - bash: yarn test:core displayName: Test Core diff --git a/benchmark/core.js b/benchmark/core.js index bb5640f9222..1ff1adb0203 100644 --- a/benchmark/core.js +++ b/benchmark/core.js @@ -18,7 +18,7 @@ const Writer = proxyquire('../src/writer', { const Sampler = require('../src/sampler') const format = require('../src/format') const encode = require('../src/encode') -const config = new Config({ service: 'benchmark' }) +const config = new Config('benchmark', '', { service: 'benchmark' }) const suite = benchmark('core') diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 00000000000..705c3432710 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [{ + "target_name": "metrics", + "sources": [ + "src/native/metrics/Collector.cpp", + "src/native/metrics/EventLoop.cpp", + "src/native/metrics/GarbageCollection.cpp", + "src/native/metrics/Heap.cpp", + "src/native/metrics/Histogram.cpp", + "src/native/metrics/Object.cpp", + "src/native/metrics/Process.cpp", + "src/native/metrics/main.cpp" + ], + "include_dirs": [ + "src", + "=0.12.1", "parent-module": "^0.1.0", "path-to-regexp": "^0.1.2", diff --git a/scripts/post_install.js b/scripts/post_install.js new file mode 100644 index 00000000000..f20778143c5 --- /dev/null +++ b/scripts/post_install.js @@ -0,0 +1,19 @@ +'use strict' + +const execSync = require('child_process').execSync + +if (process.env.DD_NATIVE_METRICS === 'true') { + try { + execSync('npm run rebuild --scripts-prepend-node-path', { stdio: [0, 1, 2] }) + } catch (e) { + /* eslint-disable no-console */ + console.log() + console.log([ + 'Compilation of native modules failed.', + 'Falling back to JavaScript only.', + 'Some functionalities may not be available.' + ].join(' ')) + console.log() + /* eslint-enable no-console */ + } +} diff --git a/src/config.js b/src/config.js index 25da9a23415..98d8e604e1c 100644 --- a/src/config.js +++ b/src/config.js @@ -5,8 +5,8 @@ const platform = require('./platform') const coalesce = require('koalas') class Config { - constructor (service, options) { - options = typeof service === 'object' ? service : options || {} + constructor (service, runtimeId, options) { + options = options || {} const enabled = coalesce(options.enabled, platform.env('DD_TRACE_ENABLED'), true) const debug = coalesce(options.debug, platform.env('DD_TRACE_DEBUG'), false) @@ -17,8 +17,7 @@ class Config { const hostname = coalesce( options.hostname, platform.env('DD_AGENT_HOST'), - platform.env('DD_TRACE_AGENT_HOSTNAME'), - 'localhost' + platform.env('DD_TRACE_AGENT_HOSTNAME') ) const port = coalesce(options.port, platform.env('DD_TRACE_AGENT_PORT'), 8126) const sampleRate = coalesce(Math.min(Math.max(options.sampleRate, 0), 1), 1) @@ -30,8 +29,8 @@ class Config { this.debug = String(debug) === 'true' this.logInjection = String(logInjection) === 'true' this.env = env - this.url = url ? new URL(url) : new URL(`${protocol}://${hostname}:${port}`) - this.tags = Object.assign({}, options.tags) + this.url = url ? new URL(url) : new URL(`${protocol}://${hostname || 'localhost'}:${port}`) + this.hostname = hostname || this.url.hostname this.flushInterval = flushInterval this.bufferSize = 100000 this.sampleRate = sampleRate @@ -39,7 +38,16 @@ class Config { this.plugins = !!plugins this.service = coalesce(options.service, platform.env('DD_SERVICE_NAME'), service, 'node') this.analytics = String(analytics) === 'true' + this.runtimeId = coalesce(runtimeId, '') + this.tags = Object.assign({ 'runtime-id': this.runtimeId }, options.tags) + this.experimental = { + runtimeMetrics: isFlagEnabled(options.experimental, 'runtimeMetrics') + } } } +function isFlagEnabled (obj, prop) { + return obj === true || (typeof obj === 'object' && obj !== null && obj[prop]) +} + module.exports = Config diff --git a/src/native/metrics/Collector.cpp b/src/native/metrics/Collector.cpp new file mode 100644 index 00000000000..457b6c8fbaa --- /dev/null +++ b/src/native/metrics/Collector.cpp @@ -0,0 +1,7 @@ +#include "Collector.hpp" + +namespace datadog { + uint64_t Collector::time_to_micro(uv_timeval_t timeval) { + return timeval.tv_sec * 1e6 + timeval.tv_usec; + } +} diff --git a/src/native/metrics/Collector.hpp b/src/native/metrics/Collector.hpp new file mode 100644 index 00000000000..4f24cc7d7c9 --- /dev/null +++ b/src/native/metrics/Collector.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include "Object.hpp" + +namespace datadog { + class Collector { + public: + virtual void inject(Object carrier) = 0; + protected: + virtual uint64_t time_to_micro(uv_timeval_t timeval); + }; +} diff --git a/src/native/metrics/EventLoop.cpp b/src/native/metrics/EventLoop.cpp new file mode 100644 index 00000000000..5509ef72ee0 --- /dev/null +++ b/src/native/metrics/EventLoop.cpp @@ -0,0 +1,49 @@ +#include "EventLoop.hpp" + +namespace datadog { + // http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop + EventLoop::EventLoop() { + uv_check_init(uv_default_loop(), &check_handle_); + uv_unref(reinterpret_cast(&check_handle_)); + + check_handle_.data = (void*)this; + check_usage_ = usage(); + } + + EventLoop::~EventLoop() { + uv_check_stop(&check_handle_); + } + + void EventLoop::check_cb (uv_check_t* handle) { + EventLoop* self = (EventLoop*)handle->data; + self->tick(); + } + + void EventLoop::tick () { + uint64_t usage = this->usage(); + + histogram_.add(usage - check_usage_); + check_usage_ = usage; + } + + void EventLoop::enable() { + uv_check_start(&check_handle_, &EventLoop::check_cb); + } + + void EventLoop::disable() { + uv_check_stop(&check_handle_); + histogram_.reset(); + } + + uint64_t EventLoop::usage() { + uv_rusage_t usage; + uv_getrusage(&usage); + + return time_to_micro(usage.ru_utime) + time_to_micro(usage.ru_stime); + } + + void EventLoop::inject(Object carrier) { + carrier.set("eventLoop", histogram_); + histogram_.reset(); + } +} diff --git a/src/native/metrics/EventLoop.hpp b/src/native/metrics/EventLoop.hpp new file mode 100644 index 00000000000..0618a79e030 --- /dev/null +++ b/src/native/metrics/EventLoop.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "Collector.hpp" +#include "Histogram.hpp" + +namespace datadog { + class EventLoop : public Collector { + public: + EventLoop(); + ~EventLoop(); + + void enable(); + void disable(); + void tick(); + void inject(Object carrier); + protected: + static void check_cb (uv_check_t* handle); + private: + uv_check_t check_handle_; + uint64_t check_usage_; + Histogram histogram_; + + uint64_t usage(); + }; +} diff --git a/src/native/metrics/GarbageCollection.cpp b/src/native/metrics/GarbageCollection.cpp new file mode 100644 index 00000000000..712568b692f --- /dev/null +++ b/src/native/metrics/GarbageCollection.cpp @@ -0,0 +1,33 @@ +#include "GarbageCollection.hpp" + +namespace datadog { + GarbageCollection::GarbageCollection() { + pause_[v8::GCType::kGCTypeAll] = Histogram(); + } + + void GarbageCollection::before(v8::GCType type) { + start_time_ = uv_hrtime(); + } + + void GarbageCollection::after(v8::GCType type) { + uint64_t usage = uv_hrtime() - start_time_; + + if (pause_.find(type) == pause_.end()) { + pause_[type] = Histogram(); + } + + pause_[type].add(usage); + pause_[v8::GCType::kGCTypeAll].add(usage); + } + + void GarbageCollection::inject(Object carrier) { + Object value; + + for (std::map::iterator it = pause_.begin(); it != pause_.end(); ++it) { + value.set(types_[it->first], it->second); + it->second.reset(); + } + + carrier.set("gc", value); + } +} diff --git a/src/native/metrics/GarbageCollection.hpp b/src/native/metrics/GarbageCollection.hpp new file mode 100644 index 00000000000..6e59c422dce --- /dev/null +++ b/src/native/metrics/GarbageCollection.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Collector.hpp" +#include "Histogram.hpp" +#include "Object.hpp" + +namespace datadog { + class GarbageCollection : public Collector { + public: + GarbageCollection(); + + void before(v8::GCType type); + void after(v8::GCType type); + void inject(Object carrier); + private: + std::map pause_; + std::map types_ = { + { 1, "scavenge" }, + { 2, "mark_sweep_compact" }, + { 3, "all" }, // Node 4 + { 4, "incremental_marking" }, + { 8, "process_weak_callbacks" }, + { 15, "all" } + }; + uint64_t start_time_; + }; +} diff --git a/src/native/metrics/Heap.cpp b/src/native/metrics/Heap.cpp new file mode 100644 index 00000000000..5e3b26d1b7a --- /dev/null +++ b/src/native/metrics/Heap.cpp @@ -0,0 +1,27 @@ +#include "Heap.hpp" + +namespace datadog { + void Heap::inject(Object carrier) { + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + Object heap; + std::vector spaces; + + for (unsigned int i = 0; i < isolate->NumberOfHeapSpaces(); i++) { + Object space; + v8::HeapSpaceStatistics stats; + + if (isolate->GetHeapSpaceStatistics(&stats, i)) { + space.set("space_name", std::string(stats.space_name())); + space.set("space_size", stats.space_size()); + space.set("space_used_size", stats.space_used_size()); + space.set("space_available_size", stats.space_available_size()); + space.set("physical_space_size", stats.physical_space_size()); + + spaces.push_back(space); + } + } + + heap.set("spaces", spaces); + carrier.set("heap", heap); + } +} diff --git a/src/native/metrics/Heap.hpp b/src/native/metrics/Heap.hpp new file mode 100644 index 00000000000..d92406a55e7 --- /dev/null +++ b/src/native/metrics/Heap.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +#include "Collector.hpp" +#include "Object.hpp" + +namespace datadog { + class Heap : public Collector { + public: + void inject(Object carrier); + }; +} diff --git a/src/native/metrics/Histogram.cpp b/src/native/metrics/Histogram.cpp new file mode 100644 index 00000000000..4733aaf6da1 --- /dev/null +++ b/src/native/metrics/Histogram.cpp @@ -0,0 +1,28 @@ +#include "Histogram.hpp" + +namespace datadog { + uint64_t Histogram::min() { return min_; } + uint64_t Histogram::max() { return max_; } + uint64_t Histogram::sum() { return sum_; } + uint64_t Histogram::avg() { return count_ == 0 ? 0 : sum_ / count_; } + uint64_t Histogram::count() { return count_; } + + void Histogram::reset() { + min_ = 0; + max_ = 0; + sum_ = 0; + count_ = 0; + } + + void Histogram::add(uint64_t value) { + if (count_ == 0) { + min_ = max_ = value; + } else { + min_ = (std::min)(min_, value); + max_ = (std::max)(max_, value); + } + + count_ += 1; + sum_ += value; + } +} diff --git a/src/native/metrics/Histogram.hpp b/src/native/metrics/Histogram.hpp new file mode 100644 index 00000000000..248e4e7fb6d --- /dev/null +++ b/src/native/metrics/Histogram.hpp @@ -0,0 +1,29 @@ +#pragma once + +// windows.h defines min and max macros. +#define NOMINMAX +#include +#undef min +#undef max +#undef NOMINMAX + +#include + +namespace datadog { + class Histogram { + public: + uint64_t min(); + uint64_t max(); + uint64_t sum(); + uint64_t avg(); + uint64_t count(); + + void reset(); + void add(uint64_t value); + private: + uint64_t min_; + uint64_t max_; + uint64_t sum_; + uint64_t count_; + }; +} diff --git a/src/native/metrics/Object.cpp b/src/native/metrics/Object.cpp new file mode 100644 index 00000000000..cb3afa732df --- /dev/null +++ b/src/native/metrics/Object.cpp @@ -0,0 +1,85 @@ +#include "Object.hpp" + +namespace datadog { + Object::Object() { + target_ = Nan::New(); + } + + Object::Object(v8::Local target) { + target_ = target; + } + + void Object::set(std::string key, std::string value) { + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + Nan::New(value).ToLocalChecked() + ); + } + + void Object::set(std::string key, uint64_t value) { + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + Nan::New(value) + ); + } + + void Object::set(std::string key, v8::Local value) { + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + value + ); + } + + void Object::set(std::string key, Object value) { + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + value.to_json() + ); + } + + void Object::set(std::string key, std::vector value) { + v8::Local array = Nan::New(value.size()); + + for (unsigned int i = 0; i < array->Length(); i++) { + Nan::Set(array, i, value.at(i).to_json()); + } + + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + array + ); + } + + void Object::set(std::string key, Histogram value) { + Object obj; + + obj.set("min", value.min()); + obj.set("max", value.max()); + obj.set("sum", value.sum()); + obj.set("avg", value.avg()); + obj.set("count", value.count()); + + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + obj.to_json() + ); + } + + void Object::set(std::string key, Nan::FunctionCallback value) { + Nan::Set( + target_, + Nan::New(key).ToLocalChecked(), + Nan::GetFunction(Nan::New(value)).ToLocalChecked() + ); + } + + v8::Local Object::to_json() { + return target_; + } +} diff --git a/src/native/metrics/Object.hpp b/src/native/metrics/Object.hpp new file mode 100644 index 00000000000..ad97c2d7863 --- /dev/null +++ b/src/native/metrics/Object.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Histogram.hpp" + +namespace datadog { + class Object { + public: + Object(); + Object(v8::Local target); + + void set(std::string key, std::string value); + void set(std::string key, uint64_t value); + void set(std::string key, v8::Local value); + void set(std::string key, Object value); + void set(std::string key, std::vector value); + void set(std::string key, Histogram value); + void set(std::string key, Nan::FunctionCallback value); + + v8::Local to_json(); + private: + v8::Local target_; + }; +} diff --git a/src/native/metrics/Process.cpp b/src/native/metrics/Process.cpp new file mode 100644 index 00000000000..4f0c35daa83 --- /dev/null +++ b/src/native/metrics/Process.cpp @@ -0,0 +1,17 @@ +#include "Process.hpp" + +namespace datadog { + void Process::inject(Object carrier) { + uv_rusage_t usage; + uv_getrusage(&usage); + + Object cpu; + + cpu.set("user", time_to_micro(usage.ru_utime) - time_to_micro(usage_.ru_utime)); + cpu.set("system", time_to_micro(usage.ru_stime) - time_to_micro(usage_.ru_stime)); + + carrier.set("cpu", cpu); + + usage_ = usage; + } +} diff --git a/src/native/metrics/Process.hpp b/src/native/metrics/Process.hpp new file mode 100644 index 00000000000..463db99bda3 --- /dev/null +++ b/src/native/metrics/Process.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include "Collector.hpp" +#include "Object.hpp" + +namespace datadog { + class Process : public Collector { + public: + void enable(); + void disable(); + void inject(Object carrier); + private: + bool enabled_; + uv_rusage_t usage_; + }; +} diff --git a/src/native/metrics/main.cpp b/src/native/metrics/main.cpp new file mode 100644 index 00000000000..c8be1901e7f --- /dev/null +++ b/src/native/metrics/main.cpp @@ -0,0 +1,60 @@ +#include + +#include "Collector.hpp" +#include "EventLoop.hpp" +#include "GarbageCollection.hpp" +#include "Heap.hpp" +#include "Object.hpp" +#include "Process.hpp" + +namespace datadog { + namespace { + EventLoop eventLoop; + GarbageCollection gc; + Heap heap; + Process process; + + NAN_GC_CALLBACK(before_gc) { + gc.before(type); + } + + NAN_GC_CALLBACK(after_gc) { + gc.after(type); + } + + NAN_METHOD(start) { + eventLoop.enable(); + + Nan::AddGCPrologueCallback(before_gc); + Nan::AddGCEpilogueCallback(after_gc); + } + + NAN_METHOD(stop) { + eventLoop.disable(); + + Nan::RemoveGCPrologueCallback(before_gc); + Nan::RemoveGCEpilogueCallback(after_gc); + } + + NAN_METHOD(stats) { + Object obj; + + eventLoop.inject(obj); + gc.inject(obj); + process.inject(obj); + heap.inject(obj); + + info.GetReturnValue().Set(obj.to_json()); + } + } + + NAN_MODULE_INIT(init) { + Object obj = Object(target); + + obj.set("start", start); + obj.set("stop", stop); + obj.set("stats", stats); + } + + NODE_MODULE(metrics, init); +} diff --git a/src/platform/node/index.js b/src/platform/node/index.js index 02d579dfae4..e3382f199f9 100644 --- a/src/platform/node/index.js +++ b/src/platform/node/index.js @@ -2,12 +2,14 @@ const EventEmitter = require('events') const id = require('./id') +const uuid = require('./uuid') const now = require('./now') const env = require('./env') const validate = require('./validate') const service = require('./service') const request = require('./request') const msgpack = require('./msgpack') +const metrics = require('./metrics') const Uint64BE = require('./uint64be') const emitter = new EventEmitter() @@ -21,12 +23,14 @@ const platform = { this._config = config }, id, + uuid, now, env, validate, service, request, msgpack, + metrics, Uint64BE, on: emitter.on.bind(emitter), once: emitter.once.bind(emitter), diff --git a/src/platform/node/metrics.js b/src/platform/node/metrics.js new file mode 100644 index 00000000000..b8e4e683737 --- /dev/null +++ b/src/platform/node/metrics.js @@ -0,0 +1,214 @@ +'use strict' + +const v8 = require('v8') +const log = require('../../log') + +const INTERVAL = 10 * 1000 + +let nativeMetrics = null + +let interval +let client +let time +let cpuUsage +let counters + +reset() + +module.exports = function () { + return { + start: () => { + const StatsD = require('hot-shots') + + try { + nativeMetrics = require('../../../build/Release/metrics') + nativeMetrics.start() + } catch (e) { + log.error('Unable to load native metrics module. Some metrics will not be available.') + } + + client = new StatsD({ + host: this._config.hostname, + port: 8125, // TODO: allow to configure this + prefix: 'runtime.node.', + globalTags: { + 'env': this._config.env, + 'service': this._config.service, + 'runtime-id': this._config.runtimeId + }, + errorHandler: () => {} + }) + + time = process.hrtime() + + if (nativeMetrics) { + interval = setInterval(() => { + captureCommonMetrics() + captureNativeMetrics() + }, INTERVAL) + } else { + cpuUsage = process.cpuUsage() + + interval = setInterval(() => { + captureCommonMetrics() + captureCpuUsage() + captureHeapSpace() + }, INTERVAL) + } + + interval.unref() + }, + + stop: () => { + if (nativeMetrics) { + nativeMetrics.stop() + } + + clearInterval(interval) + reset() + }, + + increment: (name) => { + if (!client) return + + if (counters[name] !== undefined) { + counters[name]++ + } else { + counters[name] = 1 + } + }, + + decrement: (name) => { + if (!client) return + + if (counters[name] !== undefined) { + counters[name]-- + } else { + counters[name] = -1 + } + } + } +} + +function reset () { + interval = null + client = null + time = null + cpuUsage = null + counters = {} +} + +function captureCpuUsage () { + if (!process.cpuUsage) return + + const elapsedTime = process.hrtime(time) + const elapsedUsage = process.cpuUsage(cpuUsage) + + time = process.hrtime() + cpuUsage = process.cpuUsage() + + const elapsedMs = elapsedTime[0] * 1000 + elapsedTime[1] / 1000000 + const userPercent = 100 * elapsedUsage.user / 1000 / elapsedMs + const systemPercent = 100 * elapsedUsage.system / 1000 / elapsedMs + const totalPercent = userPercent + systemPercent + + client.gauge('cpu.system', systemPercent.toFixed(2)) + client.gauge('cpu.user', userPercent.toFixed(2)) + client.gauge('cpu.total', totalPercent.toFixed(2)) +} + +function captureMemoryUsage () { + const stats = process.memoryUsage() + + client.gauge('mem.heap_total', stats.heapTotal) + client.gauge('mem.heap_used', stats.heapUsed) + client.gauge('mem.rss', stats.rss) + + stats.external && client.gauge('mem.external', stats.external) +} + +function captureProcess () { + client.gauge('process.uptime', Math.round(process.uptime())) +} + +function captureHeapStats () { + const stats = v8.getHeapStatistics() + + client.gauge('heap.total_heap_size', stats.total_heap_size) + client.gauge('heap.total_heap_size_executable', stats.total_heap_size_executable) + client.gauge('heap.total_physical_size', stats.total_physical_size) + client.gauge('heap.total_available_size', stats.total_available_size) + client.gauge('heap.total_heap_size', stats.total_heap_size) + client.gauge('heap.heap_size_limit', stats.heap_size_limit) + + stats.malloced_memory && client.gauge('heap.malloced_memory', stats.malloced_memory) + stats.peak_malloced_memory && client.gauge('heap.peak_malloced_memory', stats.peak_malloced_memory) +} + +function captureHeapSpace () { + if (!v8.getHeapSpaceStatistics) return + + const stats = v8.getHeapSpaceStatistics() + + for (let i = 0, l = stats.length; i < l; i++) { + const tags = { 'heap.space': stats[i].space_name } + + client.gauge('heap.space_size', stats[i].space_size, tags) + client.gauge('heap.space_used_size', stats[i].space_used_size, tags) + client.gauge('heap.space_available_size', stats[i].space_available_size, tags) + client.gauge('heap.physical_space_size', stats[i].physical_space_size, tags) + } +} + +function captureCounters () { + Object.keys(counters).forEach(name => { + client.gauge(name, counters[name]) + }) +} + +function captureCommonMetrics () { + captureMemoryUsage() + captureProcess() + captureHeapStats() + captureCounters() +} + +function captureNativeMetrics () { + const stats = nativeMetrics.stats() + const spaces = stats.heap.spaces + const elapsedTime = process.hrtime(time) + + time = process.hrtime() + + const elapsedUs = elapsedTime[0] * 1e6 + elapsedTime[1] / 1e3 + const userPercent = 100 * stats.cpu.user / elapsedUs + const systemPercent = 100 * stats.cpu.system / elapsedUs + const totalPercent = userPercent + systemPercent + + client.gauge('cpu.system', systemPercent.toFixed(2)) + client.gauge('cpu.user', userPercent.toFixed(2)) + client.gauge('cpu.total', totalPercent.toFixed(2)) + + client.gauge('event_loop.tick.max', stats.eventLoop.max) + client.gauge('event_loop.tick.min', stats.eventLoop.min) + client.gauge('event_loop.tick.sum', stats.eventLoop.sum) + client.gauge('event_loop.tick.avg', stats.eventLoop.avg) + client.gauge('event_loop.tick.count', stats.eventLoop.count) + + Object.keys(stats.gc).forEach(type => { + client.gauge(`gc.${type}.min`, stats.gc[type].min) + client.gauge(`gc.${type}.max`, stats.gc[type].max) + client.gauge(`gc.${type}.sum`, stats.gc[type].sum) + client.gauge(`gc.${type}.avg`, stats.gc[type].avg) + client.gauge(`gc.${type}.count`, stats.gc[type].count) + }) + + for (let i = 0, l = spaces.length; i < l; i++) { + const tags = { 'heap.space': spaces[i].space_name } + + client.gauge('heap.space_size', spaces[i].space_size, tags) + client.gauge('heap.space_used_size', spaces[i].space_used_size, tags) + client.gauge('heap.space_available_size', spaces[i].space_available_size, tags) + client.gauge('heap.physical_space_size', spaces[i].physical_space_size, tags) + } +} diff --git a/src/platform/node/uuid.js b/src/platform/node/uuid.js new file mode 100644 index 00000000000..853ca4865e2 --- /dev/null +++ b/src/platform/node/uuid.js @@ -0,0 +1,5 @@ +'use strict' + +const crypto = require('crypto') + +module.exports = (size) => crypto.randomBytes(16).slice(0, size).toString('hex') diff --git a/src/proxy.js b/src/proxy.js index 43fc98c56e0..473e3676f3b 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -26,12 +26,17 @@ class Tracer extends BaseTracer { if (this._tracer === noop) { try { const service = platform.service() - const config = new Config(service, options) + const runtimeId = platform.uuid() + const config = new Config(service, runtimeId, options) if (config.enabled) { platform.validate() platform.configure(config) + if (config.experimental.runtimeMetrics) { + platform.metrics().start() + } + this._tracer = new DatadogTracer(config) this._instrumenter.enable() this._instrumenter.patch(config) diff --git a/test/config.spec.js b/test/config.spec.js index ab6f5f1d971..dfeb6ab0013 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -26,9 +26,12 @@ describe('Config', () => { expect(config).to.have.property('flushInterval', 2000) expect(config).to.have.property('bufferSize', 100000) expect(config).to.have.property('sampleRate', 1) - expect(config).to.have.deep.property('tags', {}) + expect(config).to.have.deep.property('tags', { + 'runtime-id': '' + }) expect(config).to.have.property('plugins', true) expect(config).to.have.property('env', undefined) + expect(config).to.have.property('runtimeId', '') }) it('should initialize from the default service', () => { @@ -37,6 +40,13 @@ describe('Config', () => { expect(config).to.have.property('service', 'test') }) + it('should initialize from the provided runtime ID', () => { + const config = new Config('test', '1234') + + expect(config).to.have.property('runtimeId', '1234') + expect(config.tags).to.have.property('runtime-id', '1234') + }) + it('should initialize from environment variables', () => { platform.env.withArgs('DD_TRACE_AGENT_HOSTNAME').returns('agent') platform.env.withArgs('DD_TRACE_AGENT_PORT').returns('6218') @@ -81,7 +91,7 @@ describe('Config', () => { it('should initialize from the options', () => { const logger = {} const tags = { foo: 'bar' } - const config = new Config({ + const config = new Config('test', '', { enabled: false, debug: true, analytics: true, @@ -106,7 +116,7 @@ describe('Config', () => { expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) expect(config).to.have.property('logger', logger) - expect(config).to.have.deep.property('tags', tags) + expect(config.tags).to.have.property('foo', 'bar') expect(config).to.have.property('flushInterval', 5000) expect(config).to.have.property('plugins', false) }) @@ -114,7 +124,7 @@ describe('Config', () => { it('should initialize from the options with url taking precedence', () => { const logger = {} const tags = { foo: 'bar' } - const config = new Config({ + const config = new Config('test', '', { enabled: false, debug: true, hostname: 'agent', @@ -138,7 +148,7 @@ describe('Config', () => { expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) expect(config).to.have.property('logger', logger) - expect(config).to.have.deep.property('tags', tags) + expect(config.tags).to.have.property('foo', 'bar') expect(config).to.have.property('flushInterval', 5000) expect(config).to.have.property('plugins', false) }) @@ -162,7 +172,7 @@ describe('Config', () => { platform.env.withArgs('DD_SERVICE_NAME').returns('service') platform.env.withArgs('DD_ENV').returns('test') - const config = new Config({ + const config = new Config('test', '', { enabled: true, debug: false, analytics: false, @@ -192,7 +202,7 @@ describe('Config', () => { platform.env.withArgs('DD_SERVICE_NAME').returns('service') platform.env.withArgs('DD_ENV').returns('test') - const config = new Config({ + const config = new Config('test', '', { enabled: true, debug: false, url: 'https://agent3:7778', @@ -213,8 +223,8 @@ describe('Config', () => { }) it('should sanitize the sample rate to be between 0 and 1', () => { - expect(new Config({ sampleRate: -1 })).to.have.property('sampleRate', 0) - expect(new Config({ sampleRate: 2 })).to.have.property('sampleRate', 1) - expect(new Config({ sampleRate: NaN })).to.have.property('sampleRate', 1) + expect(new Config('test', '', { sampleRate: -1 })).to.have.property('sampleRate', 0) + expect(new Config('test', '', { sampleRate: 2 })).to.have.property('sampleRate', 1) + expect(new Config('test', '', { sampleRate: NaN })).to.have.property('sampleRate', 1) }) }) diff --git a/test/platform/node/index.spec.js b/test/platform/node/index.spec.js index 1bc7328e665..459b33b796b 100644 --- a/test/platform/node/index.spec.js +++ b/test/platform/node/index.spec.js @@ -86,6 +86,22 @@ describe('Platform', () => { }) }) + describe('uuid', () => { + let uuid + + beforeEach(() => { + uuid = require('../../../src/platform/node/uuid') + }) + + it('should return a random UUID', () => { + expect(uuid()).to.match(/^[a-f0-9]{32}$/) + }) + + it('should return part of a random UUID', () => { + expect(uuid(8)).to.match(/^[a-f0-9]{16}$/) + }) + }) + describe('now', () => { let now let performanceNow @@ -327,5 +343,162 @@ describe('Platform', () => { }) }) }) + + describe('metrics', () => { + let metrics + let clock + let client + let StatsD + + beforeEach(() => { + StatsD = sinon.spy(function () { + return client + }) + + client = { + gauge: sinon.spy(), + increment: sinon.spy() + } + + metrics = proxyquire('../src/platform/node/metrics', { + 'hot-shots': StatsD + }) + + clock = sinon.useFakeTimers() + + platform = { + _config: { + service: 'service', + env: 'test', + hostname: 'localhost', + runtimeId: '1234' + }, + name: sinon.stub().returns('nodejs'), + version: sinon.stub().returns('10.0.0'), + engine: sinon.stub().returns('v8') + } + }) + + afterEach(() => { + clock.restore() + }) + + describe('start', () => { + it('it should initialize the StatsD client with the correct options', () => { + metrics.apply(platform).start() + + expect(StatsD).to.have.been.calledWithMatch({ + host: 'localhost', + globalTags: { + 'service': 'service', + 'env': 'test', + 'runtime-id': '1234' + } + }) + }) + + it('should start collecting metrics every 10 seconds', () => { + metrics.apply(platform).start() + + clock.tick(10000) + + expect(client.gauge).to.have.been.calledWith('cpu.user') + expect(client.gauge).to.have.been.calledWith('cpu.system') + expect(client.gauge).to.have.been.calledWith('cpu.total') + + expect(client.gauge).to.have.been.calledWith('mem.rss') + expect(client.gauge).to.have.been.calledWith('mem.heap_total') + expect(client.gauge).to.have.been.calledWith('mem.heap_used') + + expect(client.gauge).to.have.been.calledWith('process.uptime') + + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size_executable') + expect(client.gauge).to.have.been.calledWith('heap.total_physical_size') + expect(client.gauge).to.have.been.calledWith('heap.total_available_size') + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('heap.heap_size_limit') + + if (semver.gte(process.version, '7.2.0')) { + expect(client.gauge).to.have.been.calledWith('heap.malloced_memory') + expect(client.gauge).to.have.been.calledWith('heap.peak_malloced_memory') + } + + expect(client.gauge).to.have.been.calledWith('event_loop.tick.max') + expect(client.gauge).to.have.been.calledWith('event_loop.tick.min') + expect(client.gauge).to.have.been.calledWith('event_loop.tick.sum') + expect(client.gauge).to.have.been.calledWith('event_loop.tick.avg') + expect(client.gauge).to.have.been.calledWith('event_loop.tick.count') + + expect(client.gauge).to.have.been.calledWith('gc.all.max') + expect(client.gauge).to.have.been.calledWith('gc.all.min') + expect(client.gauge).to.have.been.calledWith('gc.all.sum') + expect(client.gauge).to.have.been.calledWith('gc.all.avg') + expect(client.gauge).to.have.been.calledWith('gc.all.count') + + expect(client.gauge).to.have.been.calledWith('heap.space_size') + expect(client.gauge).to.have.been.calledWith('heap.space_used_size') + expect(client.gauge).to.have.been.calledWith('heap.space_available_size') + expect(client.gauge).to.have.been.calledWith('heap.physical_space_size') + }) + }) + + describe('stop', () => { + it('should stop collecting metrics every 10 seconds', () => { + metrics.apply(platform).start() + metrics.apply(platform).stop() + + clock.tick(10000) + + expect(client.gauge).to.not.have.been.called + }) + }) + + describe('without native metrics', () => { + beforeEach(() => { + metrics = proxyquire('../src/platform/node/metrics', { + 'hot-shots': StatsD, + '../../../build/Release/metrics': null + }) + }) + + it('should fallback to only metrics available to JavaScript code', () => { + metrics.apply(platform).start() + + clock.tick(10000) + + if (semver.gte(process.version, '6.1.0')) { + expect(client.gauge).to.have.been.calledWith('cpu.user') + expect(client.gauge).to.have.been.calledWith('cpu.system') + expect(client.gauge).to.have.been.calledWith('cpu.total') + } + + expect(client.gauge).to.have.been.calledWith('mem.rss') + expect(client.gauge).to.have.been.calledWith('mem.heap_total') + expect(client.gauge).to.have.been.calledWith('mem.heap_used') + + expect(client.gauge).to.have.been.calledWith('process.uptime') + + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size_executable') + expect(client.gauge).to.have.been.calledWith('heap.total_physical_size') + expect(client.gauge).to.have.been.calledWith('heap.total_available_size') + expect(client.gauge).to.have.been.calledWith('heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('heap.heap_size_limit') + + if (semver.gte(process.version, '7.2.0')) { + expect(client.gauge).to.have.been.calledWith('heap.malloced_memory') + expect(client.gauge).to.have.been.calledWith('heap.peak_malloced_memory') + } + + if (semver.gte(process.version, '6.0.0')) { + expect(client.gauge).to.have.been.calledWith('heap.space_size') + expect(client.gauge).to.have.been.calledWith('heap.space_used_size') + expect(client.gauge).to.have.been.calledWith('heap.space_available_size') + expect(client.gauge).to.have.been.calledWith('heap.physical_space_size') + } + }) + }) + }) }) }) diff --git a/test/plugins/dns.spec.js b/test/plugins/dns.spec.js index 434a972a3bb..f365fe15df6 100644 --- a/test/plugins/dns.spec.js +++ b/test/plugins/dns.spec.js @@ -27,11 +27,11 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.lookup', service: 'test-dns', - resource: 'localhost', - meta: { - 'dns.hostname': 'localhost', - 'span.kind': 'client' - } + resource: 'localhost' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.hostname': 'localhost', + 'span.kind': 'client' }) }) .then(done) @@ -46,12 +46,12 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.lookup_service', service: 'test-dns', - resource: '127.0.0.1:22', - meta: { - 'dns.address': '127.0.0.1', - 'dns.port': '22', - 'span.kind': 'client' - } + resource: '127.0.0.1:22' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.address': '127.0.0.1', + 'dns.port': '22', + 'span.kind': 'client' }) }) .then(done) @@ -66,12 +66,12 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.resolve', service: 'test-dns', - resource: 'A localhost', - meta: { - 'dns.hostname': 'localhost', - 'dns.rrtype': 'A', - 'span.kind': 'client' - } + resource: 'A localhost' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.hostname': 'localhost', + 'dns.rrtype': 'A', + 'span.kind': 'client' }) }) .then(done) @@ -86,12 +86,12 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.resolve', service: 'test-dns', - resource: 'ANY localhost', - meta: { - 'dns.hostname': 'localhost', - 'dns.rrtype': 'ANY', - 'span.kind': 'client' - } + resource: 'ANY localhost' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.hostname': 'localhost', + 'dns.rrtype': 'ANY', + 'span.kind': 'client' }) }) .then(done) @@ -106,11 +106,11 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.reverse', service: 'test-dns', - resource: '127.0.0.1', - meta: { - 'dns.ip': '127.0.0.1', - 'span.kind': 'client' - } + resource: '127.0.0.1' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.ip': '127.0.0.1', + 'span.kind': 'client' }) }) .then(done) @@ -128,12 +128,12 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'dns.resolve', service: 'test-dns', - resource: 'A localhost', - meta: { - 'dns.hostname': 'localhost', - 'dns.rrtype': 'A', - 'span.kind': 'client' - } + resource: 'A localhost' + }) + expect(traces[0][0].meta).to.deep.include({ + 'dns.hostname': 'localhost', + 'dns.rrtype': 'A', + 'span.kind': 'client' }) }) .then(done) diff --git a/test/plugins/net.spec.js b/test/plugins/net.spec.js index c51eb7cfc6e..7f0652d04a6 100644 --- a/test/plugins/net.spec.js +++ b/test/plugins/net.spec.js @@ -61,11 +61,11 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'ipc.connect', service: 'test-ipc', - resource: '/tmp/dd-trace.sock', - meta: { - 'span.kind': 'client', - 'ipc.path': '/tmp/dd-trace.sock' - } + resource: '/tmp/dd-trace.sock' + }) + expect(traces[0][0].meta).to.deep.include({ + 'span.kind': 'client', + 'ipc.path': '/tmp/dd-trace.sock' }) expect(traces[0][0].parent_id.toString()).to.equal(parent.context().toSpanId()) }) @@ -85,18 +85,18 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'tcp.connect', service: 'test-tcp', - resource: `localhost:${port}`, - meta: { - 'span.kind': 'client', - 'tcp.family': 'IPv4', - 'tcp.remote.host': 'localhost', - 'tcp.remote.address': '127.0.0.1', - 'tcp.remote.port': `${port}`, - 'tcp.local.address': '127.0.0.1', - 'tcp.local.port': `${socket.localPort}`, - 'out.host': 'localhost', - 'out.port': `${port}` - } + resource: `localhost:${port}` + }) + expect(traces[0][0].meta).to.deep.include({ + 'span.kind': 'client', + 'tcp.family': 'IPv4', + 'tcp.remote.host': 'localhost', + 'tcp.remote.address': '127.0.0.1', + 'tcp.remote.port': `${port}`, + 'tcp.local.address': '127.0.0.1', + 'tcp.local.port': `${socket.localPort}`, + 'out.host': 'localhost', + 'out.port': `${port}` }) expect(traces[0][0].parent_id.toString()).to.equal(parent.context().toSpanId()) }) @@ -116,18 +116,18 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'tcp.connect', service: 'test-tcp', - resource: `localhost:${port}`, - meta: { - 'span.kind': 'client', - 'tcp.family': 'IPv4', - 'tcp.remote.host': 'localhost', - 'tcp.remote.address': '127.0.0.1', - 'tcp.remote.port': `${port}`, - 'tcp.local.address': '127.0.0.1', - 'tcp.local.port': `${socket.localPort}`, - 'out.host': 'localhost', - 'out.port': `${port}` - } + resource: `localhost:${port}` + }) + expect(traces[0][0].meta).to.deep.include({ + 'span.kind': 'client', + 'tcp.family': 'IPv4', + 'tcp.remote.host': 'localhost', + 'tcp.remote.address': '127.0.0.1', + 'tcp.remote.port': `${port}`, + 'tcp.local.address': '127.0.0.1', + 'tcp.local.port': `${socket.localPort}`, + 'out.host': 'localhost', + 'out.port': `${port}` }) expect(traces[0][0].parent_id.toString()).to.equal(parent.context().toSpanId()) }) @@ -148,11 +148,11 @@ describe('Plugin', () => { expect(traces[0][0]).to.deep.include({ name: 'ipc.connect', service: 'test-ipc', - resource: '/tmp/dd-trace.sock', - meta: { - 'span.kind': 'client', - 'ipc.path': '/tmp/dd-trace.sock' - } + resource: '/tmp/dd-trace.sock' + }) + expect(traces[0][0].meta).to.deep.include({ + 'span.kind': 'client', + 'ipc.path': '/tmp/dd-trace.sock' }) expect(traces[0][0].parent_id.toString()).to.equal(parent.context().toSpanId()) }) diff --git a/test/plugins/util/web.spec.js b/test/plugins/util/web.spec.js index 51c117bbe72..aecceb30d3d 100644 --- a/test/plugins/util/web.spec.js +++ b/test/plugins/util/web.spec.js @@ -551,7 +551,6 @@ describe('plugins/util/web', () => { end.apply(this, arguments) try { - // console.log(tracer.scope().active()) expect(tracer.scope().active()).to.not.be.null done() } catch (e) { diff --git a/test/proxy.spec.js b/test/proxy.spec.js index f4794d303c9..77a81fe2a9f 100644 --- a/test/proxy.spec.js +++ b/test/proxy.spec.js @@ -46,11 +46,15 @@ describe('TracerProxy', () => { NoopTracer = sinon.stub().returns(noop) Instrumenter = sinon.stub().returns(instrumenter) - config = { enabled: true } + config = { enabled: true, experimental: {} } Config = sinon.stub().returns(config) platform = { - load: sinon.spy() + load: sinon.spy(), + uuid: sinon.stub().returns('1234'), + metrics: sinon.stub().returns({ + start: sinon.spy() + }) } Proxy = proxyquire('../src/proxy', { @@ -84,7 +88,7 @@ describe('TracerProxy', () => { proxy.init(options) - expect(Config).to.have.been.calledWith('dd-trace', options) + expect(Config).to.have.been.calledWith('dd-trace', '1234', options) expect(DatadogTracer).to.have.been.calledWith(config) }) @@ -115,6 +119,20 @@ describe('TracerProxy', () => { expect(instrumenter.patch).to.have.been.calledAfter(DatadogTracer) }) + + it('should not capture metrics by default', () => { + proxy.init() + + expect(platform.metrics().start).to.not.have.been.called + }) + + it('should start capturing metrics when configured', () => { + config.experimental = { runtimeMetrics: true } + + proxy.init() + + expect(platform.metrics().start).to.have.been.called + }) }) describe('trace', () => { diff --git a/test/setup/core.js b/test/setup/core.js index fdf72ddfbb9..fdc336bfe01 100644 --- a/test/setup/core.js +++ b/test/setup/core.js @@ -27,6 +27,7 @@ platform.use(node) afterEach(() => { agent.reset() + platform.metrics().stop() }) function wrapIt () { diff --git a/test/tracer.spec.js b/test/tracer.spec.js index 0ce1f8bd747..423226d3f9f 100644 --- a/test/tracer.spec.js +++ b/test/tracer.spec.js @@ -19,7 +19,7 @@ describe('Tracer', () => { let Instrumenter beforeEach(() => { - config = new Config({ service: 'service' }) + config = new Config('test', '', { service: 'service' }) instrumenter = { use: sinon.spy(),