├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── README.md ├── index.d.ts ├── index.js ├── lib ├── binary_heap.js └── utils.js ├── metrics ├── cached_gauge.js ├── counter.js ├── gauge.js ├── histogram.js ├── index.js ├── meter.js └── timer.js ├── package.json ├── reporting ├── console-reporter.js ├── csv-reporter.js ├── graphite-reporter.js ├── index.js ├── report.js ├── scheduled-reporter.js └── server.js ├── stats ├── exponentially_decaying_sample.js ├── exponentially_weighted_moving_average.js ├── index.js ├── sample.js └── uniform_sample.js ├── support ├── gitdoc.json ├── reference │ ├── Counter.md │ ├── Histogram.md │ ├── Meter.md │ ├── Report.md │ ├── Server.md │ └── Timer.md └── styles.css ├── test └── unit │ ├── cached_gauge.js │ ├── console_reporter.js │ ├── counter.js │ ├── csv_reporter.js │ ├── gauge.js │ ├── graphite_reporter.js │ ├── helper.js │ ├── histogram.js │ ├── meter.js │ ├── scheduled_reporter.js │ ├── timer.js │ └── uniform_sample.js └── tests ├── helper.js ├── test_all.js ├── test_exponentially_decaying_sample.js └── test_exponentially_weighted_moving_average.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mikejihbe] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | npm-debug.log 4 | node_modules 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "8" 5 | - "6" 6 | - "4" 7 | - "0.12" 8 | - "0.10" 9 | sudo: false 10 | cache: 11 | directories: 12 | - node_modules 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Metrics 2 | ======= 3 | 4 | [![Build Status](https://travis-ci.org/mikejihbe/metrics.svg?branch=master)](https://travis-ci.org/mikejihbe/metrics) 5 | 6 | A node.js port of codahale's metrics library: https://github.com/codahale/metrics 7 | 8 | Metrics provides an instrumentation toolkit to measure the behavior of your critical systems while they're running in production. 9 | 10 | License 11 | --------- 12 | The MIT License (MIT) 13 | Copyright (c) 2012 Mike Ihbe 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | How to Use 23 | ---------- 24 | 25 | **Import Metrics** 26 | 27 | ```javascript 28 | metrics = require('metrics'); 29 | ``` 30 | 31 | **Start a metrics Server** 32 | 33 | ```javascript 34 | var metricsServer = new metrics.Server(config.metricsPort || 9091); 35 | ``` 36 | 37 | Servers are only one way to report your metrics. It's actually a thin layer on top of metrics.Report, which you could use to build other reporting mechanisms. 38 | 39 | **Add the metrics to the server** 40 | 41 | ```javascript 42 | metricsServer.addMetric('com.co.thingA', counter); 43 | metricsServer.addMetric('com.co.thingB', hist1); 44 | metricsServer.addMetric('com.co.thingC', hist2); 45 | metricsServer.addMetric('com.co.thingD', meter); 46 | metricsServer.addMetric('com.co.thingE', timer); 47 | ``` 48 | 49 | **Setting up a Reporter** 50 | 51 | A reporting interface exists for reporting metrics on a recurring interval. Reporters can be found in [reporting/](reporting). 52 | 53 | ```javascript 54 | // Report to console every 1000ms. 55 | var report = new metrics.Report(); 56 | report.addMetric('com.co.thingA', counter); 57 | var reporter = new metrics.ConsoleReporter(report); 58 | 59 | reporter.start(1000); 60 | ``` 61 | 62 | Advanced Usage 63 | -------------- 64 | Typical production deployments have multiple node processes per server. Rather than each process exposing metrics on different ports, it makes more sense to expose the metrics from the "master" process. Writing a thin wrapper around this api to perform the process communication is trivial, with a message passing setup, the client processes could look something like this: 65 | 66 | ```javascript 67 | var Metric = exports = module.exports = function Metrics(messagePasser, eventType) { 68 | this.messagePasser = messagePasser; 69 | this.eventType = eventType; 70 | } 71 | 72 | Metric.prototype.newMetric = function(type, eventType) { 73 | this.messagePasser.sendMessage({ 74 | method: 'createMetric' 75 | , type: type 76 | , eventType: eventType 77 | }); 78 | } 79 | Metric.prototype.forwardMessage = function(method, args) { 80 | this.messagePasser.sendMessage({ 81 | method: 'updateMetric' 82 | , metricMethod: method 83 | , metricArgs: args 84 | , eventType: this.eventType 85 | }); 86 | } 87 | 88 | Metric.prototype.update = function(val) { return this.forwardMessage('update', [val]); } 89 | Metric.prototype.mark = function(n) { return this.forwardMessage('mark', [n]); } 90 | Metric.prototype.inc = function(n) { return this.forwardMessage('inc', [n]); } 91 | Metric.prototype.dec = function(n) { return this.forwardMessage('dec', [n]); } 92 | Metric.prototype.clear = function() { return this.forwardMessage('clear'); } 93 | ``` 94 | 95 | And the server side that receives the createMetric and updateMetric rpcs could look something like this: 96 | 97 | ```javascript 98 | { 99 | createMetric: function(msg) { 100 | if (metricsServer) { 101 | msg.type = msg.type[0].toUpperCase() + msg.type.substring(1) 102 | metricsServer.addMetric(msg.eventType, new metrics[msg.type]); 103 | } 104 | } 105 | updateMetric: function(msg) { 106 | if (metricsServer) { 107 | var namespaces = msg.eventType.split('.') 108 | , event = namespaces.pop() 109 | , namespace = namespaces.join('.'); 110 | var metric = metricsServer.trackedMetrics[namespace][event]; 111 | metric[msg.metricMethod].apply(metric, msg.metricArgs); 112 | } 113 | } 114 | ``` 115 | 116 | For multiple server deployments, you have more options, but the best approach will be highly application dependent. Best of luck, and always be tracking! 117 | Using the metrics server you can hit the server on your configured port and you'll get a json representation of your metrics. You should collect these periodically to generate timeseries to monitor the longterm health of your application. The metrics.Reporting object would let you write to a log periodically or however else you'd like to expose your metrics. 118 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Definitions by: Pawel Badenski 2 | 3 | import events = require("events"); 4 | 5 | declare namespace metrics { 6 | type Metric = Meter | Timer | Counter | Histogram | Gauge; 7 | 8 | type MeterPrintObj = { 9 | type: "meter", 10 | 11 | count: number, 12 | m1: number, 13 | m5: number, 14 | m15: number, 15 | mean: number, 16 | unit: "seconds" 17 | }; 18 | 19 | type Rates = { 20 | 1: number; 21 | 5: number; 22 | 15: number; 23 | mean: number; 24 | }; 25 | class Meter { 26 | type: "meter"; 27 | 28 | mark: (n: number) => void; 29 | rates: () => Rates; 30 | fifteenMinuteRate: () => number; 31 | fiveMinuteRate: () => number; 32 | oneMinuteRate: () => number; 33 | meanRate: () => number; 34 | printObj: () => MeterPrintObj; 35 | } 36 | 37 | class TimerContext { 38 | stop: () => void; 39 | } 40 | 41 | class Timer { 42 | type: "timer"; 43 | histogram: Histogram; 44 | meter: Meter; 45 | 46 | clear: () => void; 47 | update: (duration: number) => void; 48 | time: () => TimerContext; 49 | 50 | count: () => number; 51 | min: () => number; 52 | max: () => number; 53 | mean: () => number; 54 | stdDev: () => number; 55 | percentiles: (percentiles: number[]) => ({ [percentile: number]: number }); 56 | values: () => number[]; 57 | 58 | oneMinuteRate: () => number; 59 | fiveMinuteRate: () => number; 60 | fifteenMinuteRate: () => number; 61 | meanRate: () => number; 62 | tick: () => void; 63 | rates: () => Rates; 64 | 65 | printObj: () => ({ 66 | type: "timer", 67 | duration: HistogramPrintObj; 68 | rate: MeterPrintObj; 69 | }); 70 | } 71 | 72 | class Counter { 73 | type: "counter"; 74 | 75 | clear: () => void; 76 | inc: (val?: number) => void; 77 | dec: (val?: number) => void; 78 | 79 | printObj: () => ({ 80 | type: "counter"; 81 | count: number; 82 | }); 83 | } 84 | 85 | class Gauge { 86 | type: "gauge"; 87 | 88 | constructor(valueFn: () => any); 89 | value: () => any 90 | 91 | printObj: () => ({ 92 | type: "gauge"; 93 | value: any; 94 | }) 95 | } 96 | 97 | class CachedGauge extends Gauge { 98 | constructor(valueFn: () => any, expirationInMs: number); 99 | } 100 | 101 | type HistogramPrintObj = { 102 | type: "histogram", 103 | 104 | min: number, 105 | max: number, 106 | sum: number, 107 | variance: number | null, 108 | mean: number | null, 109 | std_dev: number | null, 110 | count: number, 111 | median: number, 112 | p75: number, 113 | p95: number, 114 | p99: number, 115 | p999: number 116 | }; 117 | 118 | class Histogram { 119 | type: "histogram"; 120 | sample: any; 121 | min: number; 122 | max: number; 123 | sum: number; 124 | count: number; 125 | 126 | clear: () => void; 127 | update: (value: number, timestamp?: number) => void; 128 | updateVariance: (value: number) => void; 129 | 130 | percentiles: (percentiles: number[]) => ({ [percentile: number]: number }); 131 | variance: () => number | null; 132 | mean: () => number | null; 133 | stdDev: () => number | null; 134 | values: () => number[]; 135 | 136 | printObj: () => HistogramPrintObj; 137 | } 138 | 139 | interface Metrics { 140 | meters: (Meter & { name: string })[]; 141 | timers: (Timer & { name: string })[]; 142 | counters: (Counter & { name: string })[]; 143 | histograms: (Histogram & { name: string })[]; 144 | } 145 | 146 | abstract class ScheduledReporter extends events.EventEmitter { 147 | constructor(registry: Report); 148 | start: (intervalInMs: number) => void; 149 | stop: () => void; 150 | getMetrics: () => Metrics; 151 | 152 | abstract report: () => void; 153 | } 154 | 155 | class ConsoleReporter extends ScheduledReporter { 156 | constructor(registry: Report); 157 | report: () => void; 158 | } 159 | 160 | class CsvReporter extends ScheduledReporter { 161 | constructor(registry: Report); 162 | report: () => void; 163 | write: (timestamp: number, name: string, header: string, line: string, values: any[]) => void; 164 | reportCounter: (counter: Counter, timestamp: number) => void; 165 | reportMeter: (meter: Meter, timestamp: number) => void; 166 | reportTimer: (timer: Timer, timestamp: number) => void; 167 | reportHistogram: (histogram: Histogram, timestamp: number) => void; 168 | } 169 | 170 | class GraphiteReporter extends ScheduledReporter { 171 | constructor(registry: Report); 172 | report: () => void; 173 | send: (name: string, value: number, timestamp: number) => void; 174 | reportCounter: (counter: Counter, timestamp: number) => void; 175 | reportMeter: (meter: Meter, timestamp: number) => void; 176 | reportTimer: (timer: Timer, timestamp: number) => void; 177 | reportHistogram: (histogram: Histogram, timestamp: number) => void; 178 | } 179 | 180 | class Report { 181 | addMetric: (eventName: string, metric: Metric) => void; 182 | getMetric: (eventName: string) => Metric; 183 | summary: () => { [namespace: string]: { [name: string]: Metric } }; 184 | } 185 | 186 | class EWMA { 187 | alpha: number; 188 | interval: number; 189 | initialized: boolean; 190 | currentRate: number; 191 | uncounted: number; 192 | tickInterval?: number; 193 | 194 | constructor(alpha: number, interval: number); 195 | update(n: number): void; 196 | tick(): void; 197 | rate(): number; 198 | stop(): void; 199 | 200 | static createM1EWMA(): EWMA; 201 | static createM5EWMA(): EWMA; 202 | static createM15EWMA(): EWMA; 203 | } 204 | 205 | class Sample { 206 | values: number[]; 207 | count: number; 208 | 209 | init(): void; 210 | update(val: number): void; 211 | clear(): void; 212 | size(): number; 213 | print(): void; 214 | getValues(): number[]; 215 | } 216 | 217 | class ExponentiallyDecayingSample extends Sample { 218 | limit: number; 219 | alpha: number; 220 | startTime: number; 221 | nextScaleTime: number; 222 | 223 | constructor(limit: number, alpha: number); 224 | 225 | now(): number; 226 | tick(): void; 227 | clear(): void; 228 | update(val: number, timestamp?: number): void; 229 | weight(time: number): number; 230 | rescale(): void; 231 | } 232 | } 233 | 234 | export = metrics; 235 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Metrics = require('./metrics') 2 | , Reporting = require('./reporting') 3 | , Stats = require('./stats'); 4 | 5 | exports.Histogram = Metrics.Histogram; 6 | exports.Meter = Metrics.Meter; 7 | exports.Counter = Metrics.Counter; 8 | exports.Timer = Metrics.Timer; 9 | exports.Gauge = Metrics.Gauge; 10 | exports.CachedGauge = Metrics.CachedGauge; 11 | 12 | exports.Server = Reporting.Server; 13 | exports.Report = Reporting.Report; 14 | 15 | exports.ScheduledReporter = Reporting.ScheduledReporter; 16 | exports.ConsoleReporter = Reporting.ConsoleReporter; 17 | exports.CsvReporter = Reporting.CsvReporter; 18 | exports.GraphiteReporter = Reporting.GraphiteReporter; 19 | 20 | exports.EWMA = Stats.EWMA; 21 | exports.ExponentiallyDecayingSample = Stats.ExponentiallyDecayingSample; 22 | exports.Sample = Stats.Sample; 23 | exports.UniformSample = Stats.UniformSample; 24 | 25 | exports.version = require('./package.json').version; 26 | -------------------------------------------------------------------------------- /lib/binary_heap.js: -------------------------------------------------------------------------------- 1 | // From http://eloquentjavascript.net/appendix2.html, 2 | // licensed under CCv3.0: http://creativecommons.org/licenses/by/3.0/ 3 | 4 | var utils = require('./utils') 5 | 6 | /* This acts as a ordered binary heap for any serializeable JS object or collection of such objects */ 7 | var BinaryHeap = module.exports = function BinaryHeap(scoreFunction){ 8 | this.content = []; 9 | this.scoreFunction = scoreFunction; 10 | } 11 | 12 | BinaryHeap.prototype = { 13 | 14 | getValues: function() { 15 | return this.content; 16 | }, 17 | 18 | push: function(element) { 19 | // Add the new element to the end of the array. 20 | this.content.push(element); 21 | // Allow it to bubble up. 22 | this.bubbleUp(this.content.length - 1); 23 | }, 24 | 25 | peek: function() { 26 | return this.content[0]; 27 | }, 28 | 29 | pop: function() { 30 | // Store the first element so we can return it later. 31 | var result = this.content[0]; 32 | // Get the element at the end of the array. 33 | var end = this.content.pop(); 34 | // If there are any elements left, put the end element at the 35 | // start, and let it sink down. 36 | if (this.content.length > 0) { 37 | this.content[0] = end; 38 | this.sinkDown(0); 39 | } 40 | return result; 41 | }, 42 | 43 | remove: function(node) { 44 | var len = this.content.length; 45 | // To remove a value, we must search through the array to find 46 | // it. 47 | for (var i = 0; i < len; i++) { 48 | if (this.content[i] == node) { 49 | // When it is found, the process seen in 'pop' is repeated 50 | // to fill up the hole. 51 | var end = this.content.pop(); 52 | if (i != len - 1) { 53 | this.content[i] = end; 54 | if (this.scoreFunction(end) < this.scoreFunction(node)) 55 | this.bubbleUp(i); 56 | else 57 | this.sinkDown(i); 58 | } 59 | return true; 60 | } 61 | } 62 | throw new Error("Node not found."); 63 | }, 64 | 65 | size: function() { 66 | return this.content.length; 67 | }, 68 | 69 | bubbleUp: function(n) { 70 | // Fetch the element that has to be moved. 71 | var element = this.content[n]; 72 | // When at 0, an element can not go up any further. 73 | while (n > 0) { 74 | // Compute the parent element's index, and fetch it. 75 | var parentN = Math.floor((n + 1) / 2) - 1, 76 | parent = this.content[parentN]; 77 | // Swap the elements if the parent is greater. 78 | if (this.scoreFunction(element) < this.scoreFunction(parent)) { 79 | this.content[parentN] = element; 80 | this.content[n] = parent; 81 | // Update 'n' to continue at the new position. 82 | n = parentN; 83 | } 84 | // Found a parent that is less, no need to move it further. 85 | else { 86 | break; 87 | } 88 | } 89 | }, 90 | 91 | sinkDown: function(n) { 92 | // Look up the target element and its score. 93 | var length = this.content.length, 94 | element = this.content[n], 95 | elemScore = this.scoreFunction(element); 96 | 97 | while(true) { 98 | // Compute the indices of the child elements. 99 | var child2N = (n + 1) * 2, child1N = child2N - 1; 100 | // This is used to store the new position of the element, 101 | // if any. 102 | var swap = null; 103 | // If the first child exists (is inside the array)... 104 | if (child1N < length) { 105 | // Look it up and compute its score. 106 | var child1 = this.content[child1N], 107 | child1Score = this.scoreFunction(child1); 108 | // If the score is less than our element's, we need to swap. 109 | if (child1Score < elemScore) 110 | swap = child1N; 111 | } 112 | // Do the same checks for the other child. 113 | if (child2N < length) { 114 | var child2 = this.content[child2N], 115 | child2Score = this.scoreFunction(child2); 116 | if (child2Score < (swap == null ? elemScore : child1Score)) 117 | swap = child2N; 118 | } 119 | 120 | // If the element needs to be moved, swap it, and continue. 121 | if (swap != null) { 122 | this.content[n] = this.content[swap]; 123 | this.content[swap] = element; 124 | n = swap; 125 | } 126 | // Otherwise, we are done. 127 | else { 128 | break; 129 | } 130 | } 131 | } 132 | }; 133 | 134 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | /* 3 | * Mix in the properties on an object to another object 4 | * utils.mixin(target, source, [source,] [source, etc.] [merge-flag]); 5 | * 'merge' recurses, to merge object sub-properties together instead 6 | * of just overwriting with the source object. 7 | */ 8 | exports.mixin = (function () { 9 | var _mix = function (targ, src, merge) { 10 | for (var p in src) { 11 | // Don't copy stuff from the prototype 12 | if (src.hasOwnProperty(p)) { 13 | if (merge && 14 | // Assumes the source property is an Object you can 15 | // actually recurse down into 16 | (typeof src[p] == 'object') && 17 | (src[p] !== null) && 18 | !(src[p] instanceof Array)) { 19 | // Create the source property if it doesn't exist 20 | // TODO: What if it's something weird like a String or Number? 21 | if (typeof targ[p] == 'undefined') { 22 | targ[p] = {}; 23 | } 24 | _mix(targ[p], src[p], merge); // Recurse 25 | } 26 | // If it's not a merge-copy, just set and forget 27 | else { 28 | targ[p] = src[p]; 29 | } 30 | } 31 | } 32 | }; 33 | 34 | return function () { 35 | var args = Array.prototype.slice.apply(arguments), 36 | merge = false, 37 | targ, sources; 38 | if (args.length > 2) { 39 | if (typeof args[args.length - 1] == 'boolean') { 40 | merge = args.pop(); 41 | } 42 | } 43 | targ = args.shift(); 44 | sources = args; 45 | for (var i = 0, ii = sources.length; i < ii; i++) { 46 | _mix(targ, sources[i], merge); 47 | } 48 | return targ; 49 | }; 50 | })(); 51 | 52 | -------------------------------------------------------------------------------- /metrics/cached_gauge.js: -------------------------------------------------------------------------------- 1 | var Gauge = require('./gauge'), 2 | util = require('util'); 3 | 4 | function now() { 5 | return new Date().getTime(); 6 | } 7 | 8 | function CachedGauge(valueFn, expirationInMs) { 9 | var value, lastRefresh; 10 | Gauge.call(this, function() { 11 | if (!lastRefresh || now() - lastRefresh > expirationInMs) { 12 | value = valueFn(); 13 | lastRefresh = now(); 14 | } 15 | return value; 16 | }); 17 | } 18 | 19 | util.inherits(CachedGauge, Gauge); 20 | 21 | module.exports = CachedGauge 22 | -------------------------------------------------------------------------------- /metrics/counter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple counter object 3 | */ 4 | 5 | /* JavaScript uses double-precision FP for all numeric types. 6 | * Perhaps someday we'll have native 64-bit integers that can safely be 7 | * transported via JSON without additional code, but not today. */ 8 | var MAX_COUNTER_VALUE = Math.pow(2, 32); // 4294967296 9 | 10 | var Counter = module.exports = function Counter() { 11 | this.count = 0; 12 | this.type = 'counter'; 13 | } 14 | 15 | Counter.prototype.inc = function(val) { 16 | if (val === 0) { return } 17 | if (!val) { val = 1; } 18 | this.count += val; 19 | // Wrap counter if necessary. 20 | if (this.count > MAX_COUNTER_VALUE) { 21 | this.count -= (MAX_COUNTER_VALUE + 1); 22 | } 23 | } 24 | 25 | Counter.prototype.dec = function(val) { 26 | if (val === 0) { return } 27 | if (!val) { val = 1; } 28 | this.count -= val; 29 | // Prevent counter from being decremented below zero. 30 | if (this.count < 0) { 31 | this.count = 0; 32 | } 33 | } 34 | 35 | Counter.prototype.clear = function() { 36 | this.count = 0; 37 | } 38 | 39 | Counter.prototype.printObj = function() { 40 | return {type: 'counter', count: this.count}; 41 | } 42 | -------------------------------------------------------------------------------- /metrics/gauge.js: -------------------------------------------------------------------------------- 1 | var Gauge = module.exports = function Gauge(valueFn) { 2 | this.value = valueFn; 3 | this.type = 'gauge'; 4 | } 5 | 6 | Gauge.prototype.printObj = function() { 7 | return {type: 'gauge', value: this.value()}; 8 | } 9 | -------------------------------------------------------------------------------- /metrics/histogram.js: -------------------------------------------------------------------------------- 1 | var EDS = require('../stats/exponentially_decaying_sample') 2 | , UniformSample = require('../stats/uniform_sample'); 3 | 4 | var DEFAULT_PERCENTILES = [0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.98, 0.99, 0.999]; 5 | 6 | /* 7 | * A histogram tracks the distribution of items, given a sample type 8 | */ 9 | var Histogram = module.exports = function Histogram(sample) { 10 | this.sample = sample || new EDS(1028, 0.015); 11 | this.min = null; 12 | this.max = null; 13 | this.sum = null; 14 | // These are for the Welford algorithm for calculating running variance 15 | // without floating-point doom. 16 | this.varianceM = null; 17 | this.varianceS = null; 18 | this.count = 0; 19 | this.type = 'histogram'; 20 | } 21 | 22 | Histogram.prototype.clear = function() { 23 | this.sample.clear(); 24 | this.min = null; 25 | this.max = null; 26 | this.sum = null; 27 | this.varianceM = null; 28 | this.varianceS = null; 29 | this.count = 0; 30 | } 31 | 32 | // timestamp param primarily used for testing 33 | Histogram.prototype.update = function(val, timestamp) { 34 | this.count++; 35 | this.sample.update(val, timestamp); 36 | if (this.max === null) { 37 | this.max = val; 38 | } else { 39 | this.max = val > this.max ? val : this.max; 40 | } 41 | if (this.min === null) { 42 | this.min = val; 43 | } else { 44 | this.min = val < this.min ? val : this.min; 45 | } 46 | this.sum = (this.sum === null) ? val : this.sum + val; 47 | this.updateVariance(val); 48 | } 49 | 50 | Histogram.prototype.updateVariance = function(val) { 51 | var oldVM = this.varianceM 52 | , oldVS = this.varianceS; 53 | if (this.count == 1) { 54 | this.varianceM = val; 55 | } else { 56 | this.varianceM = oldVM + (val - oldVM) / this.count; 57 | this.varianceS = oldVS + (val - oldVM) * (val - this.varianceM); 58 | } 59 | } 60 | 61 | // Pass an array of percentiles, e.g. [0.5, 0.75, 0.9, 0.99] 62 | Histogram.prototype.percentiles = function(percentiles) { 63 | if (!percentiles) { 64 | percentiles = DEFAULT_PERCENTILES; 65 | } 66 | var values = this.sample.getValues().map(function(v){ return parseFloat(v);}).sort(function(a,b){ return a-b;}) 67 | , scores = {} 68 | , percentile 69 | , pos 70 | , lower 71 | , upper; 72 | for (var i = 0; i < percentiles.length; i++) { 73 | pos = percentiles[i] * (values.length + 1); 74 | percentile = percentiles[i]; 75 | if (pos < 1) { scores[percentile] = values[0]; } 76 | else if (pos >= values.length) { scores[percentile] = values[values.length - 1]; } 77 | else { 78 | lower = values[Math.floor(pos) - 1]; 79 | upper = values[Math.ceil(pos) - 1]; 80 | scores[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower); 81 | } 82 | } 83 | return scores; 84 | } 85 | 86 | Histogram.prototype.variance = function() { 87 | return this.count < 1 ? null : this.varianceS / (this.count - 1); 88 | } 89 | 90 | Histogram.prototype.mean = function() { 91 | return this.count == 0 ? null : this.varianceM; 92 | } 93 | 94 | Histogram.prototype.stdDev = function() { 95 | return this.count < 1 ? null : Math.sqrt(this.variance()); 96 | } 97 | 98 | Histogram.prototype.values = function() { 99 | return this.sample.getValues(); 100 | } 101 | 102 | Histogram.prototype.printObj = function() { 103 | var percentiles = this.percentiles(); 104 | return { 105 | type: 'histogram' 106 | , min: this.min 107 | , max: this.max 108 | , sum: this.sum 109 | , variance: this.variance() 110 | , mean: this.mean() 111 | , std_dev: this.stdDev() 112 | , count: this.count 113 | , median: percentiles[0.5] 114 | , p75: percentiles[0.75] 115 | , p95: percentiles[0.95] 116 | , p99: percentiles[0.99] 117 | , p999: percentiles[0.999]}; 118 | } 119 | 120 | module.exports.createExponentialDecayHistogram = function(size, alpha) { return new Histogram(new EDS((size || 1028), (alpha || 0.015))); }; 121 | module.exports.createUniformHistogram = function(size) { return new Histogram(new UniformSample((size || 1028))); }; 122 | module.exports.DEFAULT_PERCENTILES = DEFAULT_PERCENTILES; 123 | -------------------------------------------------------------------------------- /metrics/index.js: -------------------------------------------------------------------------------- 1 | 2 | exports.Counter = require('./counter'); 3 | exports.Histogram = require('./histogram'); 4 | exports.Meter = require('./meter'); 5 | exports.Timer = require('./timer'); 6 | exports.Gauge = require('./gauge'); 7 | exports.CachedGauge = require('./cached_gauge'); 8 | 9 | -------------------------------------------------------------------------------- /metrics/meter.js: -------------------------------------------------------------------------------- 1 | var EWMA = require('../stats/exponentially_weighted_moving_average.js'); 2 | /* 3 | * 4 | */ 5 | var Meter = module.exports = function Meter() { 6 | this.m1Rate = EWMA.createM1EWMA(); 7 | this.m5Rate = EWMA.createM5EWMA(); 8 | this.m15Rate = EWMA.createM15EWMA(); 9 | this.count = 0; 10 | this.startTime = (new Date).getTime(); 11 | this.type = 'meter'; 12 | } 13 | 14 | // Mark the occurence of n events 15 | Meter.prototype.mark = function(n) { 16 | if (!n) { n = 1; } 17 | this.count += n; 18 | this.m1Rate.update(n); 19 | this.m5Rate.update(n); 20 | this.m15Rate.update(n); 21 | } 22 | 23 | Meter.prototype.rates = function() { 24 | return {1: this.oneMinuteRate() 25 | , 5: this.fiveMinuteRate() 26 | , 15: this.fifteenMinuteRate() 27 | , mean: this.meanRate()}; 28 | } 29 | 30 | // Rates are per second 31 | Meter.prototype.fifteenMinuteRate = function() { 32 | return this.m15Rate.rate(); 33 | } 34 | 35 | Meter.prototype.fiveMinuteRate = function() { 36 | return this.m5Rate.rate(); 37 | } 38 | 39 | Meter.prototype.oneMinuteRate = function() { 40 | return this.m1Rate.rate(); 41 | } 42 | 43 | Meter.prototype.meanRate = function() { 44 | return this.count / ((new Date).getTime() - this.startTime) * 1000; 45 | } 46 | 47 | Meter.prototype.printObj = function() { 48 | return {type: 'meter' 49 | , count: this.count 50 | , m1: this.oneMinuteRate() 51 | , m5: this.fiveMinuteRate() 52 | , m15: this.fifteenMinuteRate() 53 | , mean: this.meanRate() 54 | , unit: 'seconds'}; 55 | } 56 | 57 | Meter.prototype.tick = function(){ 58 | this.m1Rate.tick(); 59 | this.m5Rate.tick(); 60 | this.m15Rate.tick(); 61 | } 62 | -------------------------------------------------------------------------------- /metrics/timer.js: -------------------------------------------------------------------------------- 1 | var Meter = require('./meter'), 2 | Histogram = require('./histogram'), 3 | ExponentiallyDecayingSample = require('../stats/exponentially_decaying_sample'); 4 | 5 | var MILLIS_IN_SEC = 1e3; 6 | var MILLIS_IN_NANO = 1e-6; 7 | 8 | var TimerContext = function(timer) { 9 | this.timer = timer; 10 | this.start = process.hrtime(); 11 | } 12 | 13 | /** 14 | * Calls [update]{@link Timer#update} on the associated timer recording 15 | * the elapsed duration from when this context was created to now in 16 | * milliseconds. 17 | * 18 | * This may be called successive times to record multiple durations. 19 | */ 20 | TimerContext.prototype.stop = function() { 21 | var duration = process.hrtime(this.start); 22 | this.timer.update(duration[0] * MILLIS_IN_SEC + duration[1] * MILLIS_IN_NANO); 23 | } 24 | 25 | /* 26 | * Basically a timer tracks the rate of events and histograms the durations 27 | */ 28 | var Timer = module.exports = function Timer() { 29 | this.meter = new Meter(); 30 | this.histogram = new Histogram(new ExponentiallyDecayingSample(1028, 0.015)); 31 | this.clear(); 32 | this.type = 'timer'; 33 | } 34 | 35 | Timer.prototype.update = function(duration) { 36 | this.histogram.update(duration); 37 | this.meter.mark(); 38 | } 39 | 40 | /** 41 | * Creates a context used to record elapsed milliseconds between now and 42 | * when [stop]{@link TimerContext#stop} is called. 43 | * 44 | * @returns {TimerContext} 45 | */ 46 | Timer.prototype.time = function() { 47 | return new TimerContext(this) 48 | } 49 | 50 | // delegate these to histogram 51 | Timer.prototype.clear = function() { return this.histogram.clear(); } 52 | Timer.prototype.count = function() { return this.histogram.count; } 53 | Timer.prototype.min = function() { return this.histogram.min; } 54 | Timer.prototype.max = function() { return this.histogram.max; } 55 | Timer.prototype.mean = function() { return this.histogram.mean(); } 56 | Timer.prototype.stdDev = function() { return this.histogram.stdDev(); } 57 | Timer.prototype.percentiles = function(percentiles) { return this.histogram.percentiles(percentiles); } 58 | Timer.prototype.values = function() { return this.histogram.values(); } 59 | 60 | // delegate these to meter 61 | Timer.prototype.oneMinuteRate = function() { return this.meter.oneMinuteRate(); } 62 | Timer.prototype.fiveMinuteRate = function() { return this.meter.fiveMinuteRate(); } 63 | Timer.prototype.fifteenMinuteRate = function() { return this.meter.fifteenMinuteRate(); } 64 | Timer.prototype.meanRate = function() { return this.meter.meanRate(); } 65 | Timer.prototype.tick = function() { this.meter.tick(); } // primarily for testing 66 | Timer.prototype.rates = function() { return this.meter.rates(); } 67 | 68 | Timer.prototype.printObj = function() { 69 | return {type: 'timer' 70 | , duration: this.histogram.printObj() 71 | , rate: this.meter.printObj()}; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics", 3 | "description": "A node.js port of Coda Hale's metrics library. In use at Yammer.", 4 | "version": "0.1.21", 5 | "types": "index.d.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/mikejihbe/metrics.git" 9 | }, 10 | "author": "Mike Ihbe (mikeihbe.com)", 11 | "directories": { 12 | "lib": "." 13 | }, 14 | "engines": { 15 | "node": ">=0.10.x" 16 | }, 17 | "dependencies": { 18 | "events": "^2.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^8.0.24", 22 | "chai": "~3.5.0", 23 | "chai-string": "~1.3.0", 24 | "debug": "^2.5.2", 25 | "mocha": "^2.4.5", 26 | "typescript": "^2.4.2" 27 | }, 28 | "scripts": { 29 | "test": "tsc index.d.ts && ./node_modules/.bin/mocha test/unit" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /reporting/console-reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var ScheduledReporter = require('./scheduled-reporter.js'), 3 | Histogram = require('../metrics').Histogram, 4 | util = require('util'); 5 | 6 | /** 7 | * A custom reporter that prints all metrics on console.log at a defined interval. 8 | * @param {Report} registry report instance whose metrics to report on. 9 | * @constructor 10 | */ 11 | function ConsoleReporter(registry) { 12 | ConsoleReporter.super_.call(this, registry); 13 | } 14 | 15 | util.inherits(ConsoleReporter, ScheduledReporter); 16 | 17 | ConsoleReporter.prototype.report = function() { 18 | var metrics = this.getMetrics(); 19 | 20 | if(metrics.counters.length != 0) { 21 | printWithBanner('Counters'); 22 | metrics.counters.forEach(function (counter) { 23 | printCounter(counter); 24 | }); 25 | console.log(); 26 | } 27 | 28 | if(metrics.meters.length != 0) { 29 | printWithBanner('Meters'); 30 | metrics.meters.forEach(function (meter) { 31 | printMeter(meter); 32 | }); 33 | console.log(); 34 | } 35 | 36 | if(metrics.timers.length != 0) { 37 | printWithBanner('Timers'); 38 | metrics.timers.forEach(function (timer) { 39 | // Don't log timer if its recorded no metrics. 40 | if(timer.min() != null) { 41 | printTimer(timer); 42 | } 43 | }); 44 | console.log(); 45 | } 46 | 47 | if(metrics.histograms.length != 0) { 48 | printWithBanner('Histograms'); 49 | metrics.histograms.forEach(function (histogram) { 50 | // Don't log histogram if its recorded no metrics. 51 | if(histogram.min != null) { 52 | printHistogram(histogram); 53 | } 54 | }); 55 | console.log(); 56 | } 57 | 58 | if(metrics.gauges.length != 0) { 59 | printWithBanner('Gauges'); 60 | metrics.gauges.forEach(function (gauge) { 61 | printGauge(gauge); 62 | }); 63 | console.log(); 64 | } 65 | }; 66 | 67 | function printWithBanner(name) { 68 | var dashLength = 80 - name.length - 1; 69 | var dashes = ""; 70 | for(var i = 0; i < dashLength; i++) { 71 | dashes += '-'; 72 | } 73 | console.log('%s %s', name, dashes); 74 | } 75 | 76 | function ff(value) { 77 | var fixed = value.toFixed(2); 78 | return fixed >= 10 || fixed < 0 ? fixed : " " + fixed; 79 | } 80 | 81 | function printCounter(counter) { 82 | console.log(counter.name); 83 | console.log(' count = %d', counter.count); 84 | } 85 | 86 | function printMeter(meter) { 87 | console.log(meter.name); 88 | console.log(' count = %d', meter.count); 89 | console.log(' mean rate = %s events/%s', ff(meter.meanRate()), 'second'); 90 | console.log(' 1-minute rate = %s events/%s', ff(meter.oneMinuteRate()), 'second'); 91 | console.log(' 5-minute rate = %s events/%s', ff(meter.fiveMinuteRate()), 'second'); 92 | console.log(' 15-minute rate = %s events/%s', ff(meter.fifteenMinuteRate()), 'second'); 93 | } 94 | 95 | function printTimer(timer) { 96 | console.log(timer.name); 97 | console.log(' count = %d', timer.count()); 98 | console.log(' mean rate = %s events/%s', ff(timer.meanRate()), 'second'); 99 | console.log(' 1-minute rate = %s events/%s', ff(timer.oneMinuteRate()), 'second'); 100 | console.log(' 5-minute rate = %s events/%s', ff(timer.fiveMinuteRate()), 'second'); 101 | console.log(' 15-minute rate = %s events/%s', ff(timer.fifteenMinuteRate()), 'second'); 102 | 103 | printHistogram(timer); 104 | } 105 | 106 | function printHistogram(histogram) { 107 | var isHisto = Object.getPrototypeOf(histogram) === Histogram.prototype; 108 | if(isHisto) { 109 | // log name and count if a histogram, otherwise assume this metric is being 110 | // printed as part of another (like a timer). 111 | console.log(histogram.name); 112 | console.log(' count = %d', histogram.count); 113 | } 114 | 115 | var percentiles = histogram.percentiles([.50,.75,.95,.98,.99,.999]); 116 | // assume timer if not a histogram, in which case we include durations. 117 | var durationUnit = isHisto ? '' : ' milliseconds'; 118 | 119 | console.log(' min = %s%s', ff(isHisto ? histogram.min : histogram.min()), durationUnit); 120 | console.log(' max = %s%s', ff(isHisto ? histogram.max : histogram.max()), durationUnit); 121 | console.log(' mean = %s%s', ff(histogram.mean()), durationUnit); 122 | console.log(' stddev = %s%s', ff(histogram.stdDev()), durationUnit); 123 | console.log(' 50%% <= %s%s', ff(percentiles[.50]), durationUnit); 124 | console.log(' 75%% <= %s%s', ff(percentiles[.75]), durationUnit); 125 | console.log(' 95%% <= %s%s', ff(percentiles[.95]), durationUnit); 126 | console.log(' 98%% <= %s%s', ff(percentiles[.98]), durationUnit); 127 | console.log(' 99%% <= %s%s', ff(percentiles[.99]), durationUnit); 128 | console.log(' 99.9%% <= %s%s', ff(percentiles[.999]), durationUnit); 129 | } 130 | 131 | function printGauge(gauge) { 132 | console.log(gauge.name); 133 | console.log(' value = %s', JSON.stringify(gauge.value())); 134 | } 135 | 136 | module.exports = ConsoleReporter; 137 | 138 | -------------------------------------------------------------------------------- /reporting/csv-reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var ScheduledReporter = require('./scheduled-reporter.js'), 3 | Histogram = require('../metrics').Histogram, 4 | util = require('util'), 5 | fs = require('fs'), 6 | noop = function () {}; 7 | 8 | /** 9 | * A custom reporter that will create a csv file for each metric that is appended to on the defined reporting interval. 10 | * @param {Report} registry report instance whose metrics to report on. 11 | * @param {String} directory Directory to put csv files in. 12 | * @constructor 13 | */ 14 | function CsvReporter(registry, directory) { 15 | CsvReporter.super_.call(this, registry); 16 | this.directory = directory; 17 | } 18 | 19 | util.inherits(CsvReporter, ScheduledReporter); 20 | 21 | CsvReporter.prototype.report = function() { 22 | var metrics = this.getMetrics(); 23 | var self = this; 24 | 25 | var timestamp = (new Date).getTime() / 1000; 26 | 27 | metrics.counters.forEach(function(counter) { 28 | self.reportCounter.bind(self)(counter, timestamp); 29 | }); 30 | 31 | metrics.meters.forEach(function(meter) { 32 | self.reportMeter.bind(self)(meter, timestamp); 33 | }); 34 | 35 | metrics.timers.forEach(function(timer) { 36 | // Don't log timer if its recorded no metrics. 37 | if(timer.min() != null) { 38 | self.reportTimer.bind(self)(timer, timestamp); 39 | } 40 | }); 41 | 42 | metrics.histograms.forEach(function(histogram) { 43 | // Don't log histogram if its recorded no metrics. 44 | if(histogram.min != null) { 45 | self.reportHistogram.bind(self)(histogram, timestamp); 46 | } 47 | }); 48 | 49 | metrics.gauges.forEach(function(gauge) { 50 | self.reportGauge.bind(self)(gauge, timestamp); 51 | }); 52 | }; 53 | 54 | CsvReporter.prototype.write = function(timestamp, name, header, line, values) { 55 | var file = util.format('%s/%s.csv', this.directory, name); 56 | // copy params and add line to the beginning and pass as arguments to util.format 57 | // this operates like a quasi 'vsprintf'. 58 | var params = values.slice(); 59 | params.unshift(line + "\n"); 60 | var data = util.format.apply(util, params); 61 | data = util.format('%d,%s', timestamp, data); 62 | fs.exists(file, function(exists) { 63 | if(!exists) { 64 | // include header if file didn't previously exist 65 | data = util.format("t,%s\n%s", header, data); 66 | } 67 | fs.appendFile(file, data, {}, noop); 68 | }); 69 | }; 70 | 71 | CsvReporter.prototype.reportCounter = function(counter, timestamp) { 72 | var write = this.write.bind(this); 73 | 74 | write(timestamp, counter.name, 'count', '%d', [counter.count]); 75 | }; 76 | 77 | CsvReporter.prototype.reportMeter = function(meter, timestamp) { 78 | var write = this.write.bind(this); 79 | 80 | write(timestamp, meter.name, 81 | 'count,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit', 82 | '%d,%d,%d,%d,%d,events/%s', [ 83 | meter.count, 84 | meter.meanRate(), 85 | meter.oneMinuteRate(), 86 | meter.fiveMinuteRate(), 87 | meter.fifteenMinuteRate(), 88 | 'second' 89 | ] 90 | ); 91 | }; 92 | 93 | CsvReporter.prototype.reportTimer = function(timer, timestamp) { 94 | var write = this.write.bind(this); 95 | var percentiles = timer.percentiles([.50,.75,.95,.98,.99,.999]); 96 | 97 | write(timestamp, timer.name, 98 | 'count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999,mean_rate,m1_rate,' + 99 | 'm5_rate,m15_rate,rate_unit,duration_unit', 100 | '%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,calls/%s,%s', [ 101 | timer.count(), 102 | timer.max(), 103 | timer.mean(), 104 | timer.min(), 105 | timer.stdDev(), 106 | percentiles[.50], 107 | percentiles[.75], 108 | percentiles[.95], 109 | percentiles[.98], 110 | percentiles[.99], 111 | percentiles[.999], 112 | timer.meanRate(), 113 | timer.oneMinuteRate(), 114 | timer.fiveMinuteRate(), 115 | timer.fifteenMinuteRate(), 116 | 'second', 117 | 'millisecond' 118 | ]); 119 | }; 120 | 121 | CsvReporter.prototype.reportHistogram = function(histogram, timestamp) { 122 | var write = this.write.bind(this); 123 | var percentiles = histogram.percentiles([.50,.75,.95,.98,.99,.999]); 124 | 125 | write(timestamp, histogram.name, 126 | 'count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999', 127 | '%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d', [ 128 | histogram.count, 129 | histogram.max, 130 | histogram.mean(), 131 | histogram.min, 132 | histogram.stdDev(), 133 | percentiles[.50], 134 | percentiles[.75], 135 | percentiles[.95], 136 | percentiles[.98], 137 | percentiles[.99], 138 | percentiles[.999] 139 | ]); 140 | }; 141 | 142 | CsvReporter.prototype.reportGauge = function(gauge, timestamp) { 143 | var write = this.write.bind(this); 144 | write(timestamp, gauge.name, 'value', '%s', [JSON.stringify(gauge.value())]); 145 | } 146 | 147 | module.exports = CsvReporter; 148 | -------------------------------------------------------------------------------- /reporting/graphite-reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var ScheduledReporter = require('./scheduled-reporter.js'), 3 | Histogram = require('../metrics').Histogram, 4 | util = require('util'), 5 | Socket = require('net').Socket; 6 | 7 | var reconnecting = false; 8 | 9 | /** 10 | * A custom reporter that sends metrics to a graphite server on the carbon tcp interface. 11 | * @param {Report} registry report instance whose metrics to report on. 12 | * @param {String} prefix A string to prefix on each metric (i.e. app.hostserver) 13 | * @param {String} host The ip or hostname of the target graphite server. 14 | * @param {String} port The port graphite is running on, defaults to 2003 if not specified. 15 | * @constructor 16 | */ 17 | function GraphiteReporter(registry, prefix, host, port) { 18 | GraphiteReporter.super_.call(this, registry); 19 | this.prefix = prefix; 20 | this.host = host; 21 | this.port = port || 2003; 22 | } 23 | 24 | util.inherits(GraphiteReporter, ScheduledReporter); 25 | 26 | GraphiteReporter.prototype.start = function(intervalInMs) { 27 | var self = this; 28 | this.socket = new Socket(); 29 | this.socket.on('error', function(exc) { 30 | if(!reconnecting) { 31 | reconnecting = true; 32 | self.emit('log', 'warn', util.format('Lost connection to %s. Will reconnect in 10 seconds.', self.host), exc); 33 | // Stop the reporter and try again in 1 second. 34 | self.stop(); 35 | setTimeout(function () { 36 | reconnecting = false; 37 | self.start(intervalInMs); 38 | }, 10000); 39 | } 40 | }); 41 | 42 | self.emit('log', 'verbose', util.format("Connecting to graphite @ %s:%d", this.host, this.port)); 43 | this.socket.connect(this.port, this.host, function() { 44 | self.emit('log', 'verbose', util.format('Successfully connected to graphite @ %s:%d.', self.host, self.port)); 45 | GraphiteReporter.super_.prototype.start.call(self, intervalInMs); 46 | }); 47 | }; 48 | 49 | GraphiteReporter.prototype.stop = function() { 50 | GraphiteReporter.super_.prototype.stop.call(this); 51 | this.socket.end(); 52 | }; 53 | 54 | GraphiteReporter.prototype.report = function() { 55 | // Don't report while reconnecting. 56 | if(reconnecting) { 57 | return; 58 | } 59 | var metrics = this.getMetrics(); 60 | var self = this; 61 | var timestamp = Math.floor(Date.now() / 1000); 62 | 63 | if(metrics.counters.length != 0) { 64 | metrics.counters.forEach(function (count) { 65 | self.reportCounter.bind(self)(count, timestamp); 66 | }) 67 | } 68 | 69 | if(metrics.meters.length != 0) { 70 | metrics.meters.forEach(function (meter) { 71 | self.reportMeter.bind(self)(meter, timestamp); 72 | }) 73 | } 74 | 75 | if(metrics.timers.length != 0) { 76 | metrics.timers.forEach(function (timer) { 77 | // Don't log timer if its recorded no metrics. 78 | if(timer.min() != null) { 79 | self.reportTimer.bind(self)(timer, timestamp); 80 | } 81 | }) 82 | } 83 | 84 | if(metrics.histograms.length != 0) { 85 | metrics.histograms.forEach(function (histogram) { 86 | // Don't log histogram if its recorded no metrics. 87 | if(histogram.min != null) { 88 | self.reportHistogram.bind(self)(histogram, timestamp); 89 | } 90 | }) 91 | } 92 | 93 | if(metrics.gauges.length != 0) { 94 | metrics.gauges.forEach(function (gauge) { 95 | self.reportGauge.bind(self)(gauge, timestamp); 96 | }) 97 | } 98 | }; 99 | 100 | GraphiteReporter.prototype.send = function(name, value, timestamp) { 101 | if(reconnecting) { 102 | return; 103 | } 104 | this.socket.write(util.format('%s.%s %s %s\n', this.prefix, name, value, 105 | timestamp)); 106 | }; 107 | 108 | GraphiteReporter.prototype.reportCounter = function(counter, timestamp) { 109 | var send = this.send.bind(this); 110 | 111 | send(counter.name, counter.count, timestamp); 112 | }; 113 | 114 | GraphiteReporter.prototype.reportMeter = function(meter, timestamp) { 115 | var send = this.send.bind(this); 116 | 117 | send(util.format('%s.%s', meter.name, 'count'), meter.count, timestamp); 118 | send(util.format('%s.%s', meter.name, 'mean_rate'), meter.meanRate(), timestamp); 119 | send(util.format('%s.%s', meter.name, 'm1_rate'), meter.oneMinuteRate(), 120 | timestamp); 121 | send(util.format('%s.%s', meter.name, 'm5_rate'), meter.fiveMinuteRate(), 122 | timestamp); 123 | send(util.format('%s.%s', meter.name, 'm15_rate'), meter.fifteenMinuteRate(), 124 | timestamp); 125 | }; 126 | 127 | GraphiteReporter.prototype.reportTimer = function(timer, timestamp) { 128 | var send = this.send.bind(this); 129 | send(util.format('%s.%s', timer.name, 'count'), timer.count(), timestamp); 130 | send(util.format('%s.%s', timer.name, 'mean_rate'), timer.meanRate(), timestamp); 131 | send(util.format('%s.%s', timer.name, 'm1_rate'), timer.oneMinuteRate(), 132 | timestamp); 133 | send(util.format('%s.%s', timer.name, 'm5_rate'), timer.fiveMinuteRate(), 134 | timestamp); 135 | send(util.format('%s.%s', timer.name, 'm15_rate'), timer.fifteenMinuteRate(), 136 | timestamp); 137 | 138 | this.reportHistogram(timer, timestamp); 139 | }; 140 | 141 | GraphiteReporter.prototype.reportHistogram = function(histogram, timestamp) { 142 | var send = this.send.bind(this); 143 | 144 | var isHisto = Object.getPrototypeOf(histogram) === Histogram.prototype; 145 | if (isHisto) { 146 | // send count if a histogram, otherwise assume this metric is being 147 | // printed as part of another (like a timer). 148 | send(util.format('%s.%s', histogram.name, 'count'), histogram.count, timestamp); 149 | } 150 | 151 | var percentiles = histogram.percentiles([.50,.75,.95,.98,.99,.999]); 152 | send(util.format('%s.%s', histogram.name, 'min'), isHisto? histogram.min : histogram.min(), timestamp); 153 | send(util.format('%s.%s', histogram.name, 'mean'), histogram.mean(), timestamp); 154 | send(util.format('%s.%s', histogram.name, 'max'), isHisto ? histogram.max: histogram.max(), timestamp); 155 | send(util.format('%s.%s', histogram.name, 'stddev'), histogram.stdDev(), timestamp); 156 | send(util.format('%s.%s', histogram.name, 'p50'), percentiles[.50], timestamp); 157 | send(util.format('%s.%s', histogram.name, 'p75'), percentiles[.75], timestamp); 158 | send(util.format('%s.%s', histogram.name, 'p95'), percentiles[.95], timestamp); 159 | send(util.format('%s.%s', histogram.name, 'p98'), percentiles[.98], timestamp); 160 | send(util.format('%s.%s', histogram.name, 'p99'), percentiles[.99], timestamp); 161 | send(util.format('%s.%s', histogram.name, 'p999'), percentiles[.999], timestamp); 162 | }; 163 | 164 | GraphiteReporter.prototype.reportGauge = function(gauge, timestamp) { 165 | var send = this.send.bind(this); 166 | send(util.format('%s.%s', gauge.name, 'value'), JSON.stringify(gauge.value()), timestamp); 167 | }; 168 | 169 | module.exports = GraphiteReporter; 170 | -------------------------------------------------------------------------------- /reporting/index.js: -------------------------------------------------------------------------------- 1 | exports.Report = require('./report'); 2 | exports.Server = require('./server'); 3 | exports.ScheduledReporter = require('./scheduled-reporter'); 4 | exports.ConsoleReporter = require('./console-reporter'); 5 | exports.CsvReporter = require('./csv-reporter'); 6 | exports.GraphiteReporter = require('./graphite-reporter'); 7 | -------------------------------------------------------------------------------- /reporting/report.js: -------------------------------------------------------------------------------- 1 | /** 2 | * trackedMetrics is an object with eventTypes as keys and metrics object as values. 3 | */ 4 | 5 | var _evtparse = function (eventName){ 6 | var namespaces = eventName.split('.') 7 | , name = namespaces.pop() 8 | , namespace = namespaces.join('.'); 9 | 10 | return { 11 | ns: namespace 12 | , name: name 13 | } 14 | } 15 | 16 | var Report = module.exports = function (trackedMetrics){ 17 | this.trackedMetrics = trackedMetrics || {}; 18 | } 19 | 20 | Report.prototype.addMetric = function(eventName, metric) { 21 | var parts = _evtparse(eventName); 22 | 23 | if (!this.trackedMetrics[parts.ns]) { 24 | this.trackedMetrics[parts.ns] = {}; 25 | } 26 | if(!this.trackedMetrics[parts.ns][parts.name]) { 27 | this.trackedMetrics[parts.ns][parts.name] = metric; 28 | } 29 | } 30 | 31 | Report.prototype.getMetric = function (eventName){ 32 | var parts = _evtparse(eventName); 33 | if (!this.trackedMetrics[parts.ns]){ return; } 34 | return this.trackedMetrics[parts.ns][parts.name]; 35 | } 36 | 37 | Report.prototype.summary = function (){ 38 | var metricsObj = {}; 39 | for (var namespace in this.trackedMetrics) { 40 | metricsObj[namespace] = {}; 41 | for (var name in this.trackedMetrics[namespace]) { 42 | metricsObj[namespace][name] = this.trackedMetrics[namespace][name].printObj(); 43 | } 44 | } 45 | return metricsObj; 46 | } 47 | -------------------------------------------------------------------------------- /reporting/scheduled-reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Counter = require('../metrics').Counter, 3 | Histogram = require('../metrics').Histogram, 4 | Meter = require('../metrics').Meter, 5 | Timer = require('../metrics').Timer, 6 | Gauge = require('../metrics').Gauge, 7 | util = require('util'), 8 | EventEmitter = require('events').EventEmitter; 9 | 10 | /** 11 | * Creates a scheduled reporter instance with a given report instance. This is meant to be extended and not used on 12 | * its own, see {@link GraphiteReporter} for an example implementation. 13 | * @param {Report} registry report instance whose metrics to report on. 14 | * @constructor 15 | */ 16 | function ScheduledReporter(registry) { 17 | ScheduledReporter.super_.call(this); 18 | this.registry = registry; 19 | } 20 | 21 | util.inherits(ScheduledReporter, EventEmitter); 22 | 23 | /** 24 | * Starts the calling {@link ScheduledReporter.report} on a scheduled interval. 25 | * @param {Number} intervalInMs Number How often to report in milliseconds. 26 | */ 27 | ScheduledReporter.prototype.start = function(intervalInMs) { 28 | this.interval = setInterval(this.report.bind(this), intervalInMs); 29 | }; 30 | 31 | /** 32 | * Stops the reporter if it was previously started. 33 | */ 34 | ScheduledReporter.prototype.stop = function() { 35 | if('interval' in this) { 36 | clearInterval(this.interval); 37 | } 38 | }; 39 | 40 | /** 41 | * Method that is called on every intervalInMs that was passed into {@link ScheduledReporter.start}. This method 42 | * does nothing and should be overridden by implementers. 43 | */ 44 | ScheduledReporter.prototype.report = function() { 45 | // implemented by children. 46 | }; 47 | 48 | /** 49 | * Retrieve the metrics associated with the report given to this reporter in a format that's easy to consume 50 | * by reporters. That is an object with separate references for meters, timers counters, and histograms. 51 | * @returns {{meters: Array, timers: Array, counters: Array}} 52 | */ 53 | ScheduledReporter.prototype.getMetrics = function() { 54 | var meters = []; 55 | var timers = []; 56 | var counters = []; 57 | var histograms = []; 58 | var gauges = []; 59 | 60 | var trackedMetrics = this.registry.trackedMetrics; 61 | // Flatten metric name to be namespace.name is has a namespace and separate out metrics 62 | // by type. 63 | for(var namespace in trackedMetrics) { 64 | for(var name in trackedMetrics[namespace]) { 65 | var metric = trackedMetrics[namespace][name]; 66 | if(namespace.length > 0) { 67 | metric.name = namespace + '.' + name; 68 | } else { 69 | metric.name = name; 70 | } 71 | if(metric instanceof Meter) { 72 | meters.push(metric); 73 | } else if(metric instanceof Timer) { 74 | timers.push(metric); 75 | } else if(metric instanceof Counter) { 76 | counters.push(metric); 77 | } else if(metric instanceof Histogram) { 78 | histograms.push(metric); 79 | } else if(metric instanceof Gauge) { 80 | gauges.push(metric); 81 | } 82 | } 83 | } 84 | 85 | return { meters: meters, timers: timers, counters: counters, histograms: histograms, gauges: gauges }; 86 | }; 87 | 88 | module.exports = ScheduledReporter; 89 | -------------------------------------------------------------------------------- /reporting/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , Report = require('./report'); 3 | 4 | 5 | /** 6 | * This server will print the object upon request. The user should update the metrics 7 | * as normal within their application. 8 | */ 9 | var Server = module.exports = function Server(port, trackedMetrics) { 10 | var self = this; 11 | this.report = new Report(trackedMetrics); 12 | 13 | this.server = http.createServer(function (req, res) { 14 | if (req.url.match(/^\/metrics/)) { 15 | res.writeHead(200, {'Content-Type': 'application/json'}); 16 | res.end(JSON.stringify(self.report.summary())); 17 | } else { 18 | res.writeHead(404, {'Content-Type': 'text/plain'}); 19 | res.end('Try hitting /metrics instead'); 20 | } 21 | }).listen(port); 22 | } 23 | 24 | /** 25 | * Adds a metric to be tracked by this server 26 | */ 27 | Server.prototype.addMetric = function (){ 28 | this.report.addMetric.apply(this.report, arguments); 29 | } 30 | -------------------------------------------------------------------------------- /stats/exponentially_decaying_sample.js: -------------------------------------------------------------------------------- 1 | var Sample = require('./sample') 2 | , BinaryHeap = require('../lib/binary_heap'); 3 | 4 | /* 5 | * Take an exponentially decaying sample of size size of all values 6 | */ 7 | var RESCALE_THRESHOLD = 60 * 60 * 1000; // 1 hour in milliseconds 8 | 9 | var ExponentiallyDecayingSample = module.exports = function ExponentiallyDecayingSample(size, alpha) { 10 | this.limit = size; 11 | this.alpha = alpha; 12 | this.clear(); 13 | } 14 | 15 | ExponentiallyDecayingSample.prototype = new Sample(); 16 | 17 | // The result is not sorted in a meaningful order. 18 | ExponentiallyDecayingSample.prototype.getValues = function() { 19 | return this.values.getValues().map(function(v) { return v.val; }); 20 | } 21 | 22 | ExponentiallyDecayingSample.prototype.size = function() { 23 | return this.values.size(); 24 | } 25 | 26 | ExponentiallyDecayingSample.prototype.newHeap = function() { 27 | return new BinaryHeap(function(obj){return obj.priority;}); 28 | } 29 | 30 | ExponentiallyDecayingSample.prototype.now = function() { 31 | return (new Date()).getTime(); 32 | } 33 | 34 | ExponentiallyDecayingSample.prototype.tick = function() { 35 | return this.now() / 1000; 36 | } 37 | 38 | ExponentiallyDecayingSample.prototype.clear = function() { 39 | this.values = this.newHeap(); 40 | this.count = 0; 41 | this.startTime = this.tick(); 42 | this.nextScaleTime = this.now() + RESCALE_THRESHOLD; 43 | } 44 | 45 | /* 46 | * timestamp in milliseconds 47 | */ 48 | ExponentiallyDecayingSample.prototype.update = function(val, timestamp) { 49 | // Convert timestamp to seconds 50 | if (timestamp == undefined) { 51 | timestamp = this.tick(); 52 | } else { 53 | timestamp = timestamp / 1000; 54 | } 55 | var priority = this.weight(timestamp - this.startTime) / Math.random() 56 | , value = {val: val, priority: priority}; 57 | if (this.count < this.limit) { 58 | this.count += 1; 59 | this.values.push(value); 60 | } else { 61 | var first = this.values.peek(); 62 | if (first.priority < priority) { 63 | this.values.push(value); 64 | this.values.pop(); 65 | } 66 | } 67 | 68 | if (this.now() > this.nextScaleTime) { 69 | this.rescale(this.now(), this.nextScaleTime); 70 | } 71 | } 72 | 73 | ExponentiallyDecayingSample.prototype.weight = function(time) { 74 | return Math.exp(this.alpha * time); 75 | } 76 | 77 | // now: parameter primarily used for testing rescales 78 | ExponentiallyDecayingSample.prototype.rescale = function(now) { 79 | this.nextScaleTime = this.now() + RESCALE_THRESHOLD; 80 | var oldContent = this.values.content 81 | , newContent = [] 82 | , oldStartTime = this.startTime; 83 | this.startTime = (now && now / 1000) || this.tick(); 84 | // Downscale every priority by the same factor. Order is unaffected, which is why we're avoiding the cost of popping. 85 | for(var i = 0; i < oldContent.length; i++) { 86 | newContent.push({val: oldContent[i].val, priority: oldContent[i].priority * Math.exp(-this.alpha * (this.startTime - oldStartTime))}); 87 | } 88 | this.values.content = newContent; 89 | } 90 | -------------------------------------------------------------------------------- /stats/exponentially_weighted_moving_average.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Exponentially weighted moving average. 3 | * Args: 4 | * - alpha: 5 | * - interval: time in milliseconds 6 | */ 7 | 8 | var M1_ALPHA = 1 - Math.exp(-5/60); 9 | var M5_ALPHA = 1 - Math.exp(-5/60/5); 10 | var M15_ALPHA = 1 - Math.exp(-5/60/15); 11 | 12 | var EWMA = module.exports = function(alpha, interval) { 13 | var self = this; 14 | this.alpha = alpha; 15 | this.interval = interval || 5000; 16 | this.initialized = false; 17 | this.currentRate = 0.0; 18 | this.uncounted = 0; 19 | if (interval) { 20 | this.tickInterval = setInterval(function(){ self.tick(); }, interval); 21 | 22 | if (this.tickInterval.unref) { 23 | // Don't keep the process open if this is the last thing in the event loop. 24 | this.tickInterval.unref(); 25 | } 26 | } 27 | } 28 | 29 | EWMA.prototype.update = function(n) { 30 | this.uncounted += (n || 1); 31 | } 32 | 33 | /* 34 | * Update our rate measurements every interval 35 | */ 36 | EWMA.prototype.tick = function() { 37 | var instantRate = this.uncounted / this.interval; 38 | this.uncounted = 0; 39 | 40 | if(this.initialized) { 41 | this.currentRate += this.alpha * (instantRate - this.currentRate); 42 | } else { 43 | this.currentRate = instantRate; 44 | this.initialized = true; 45 | } 46 | } 47 | 48 | /* 49 | * Return the rate per second 50 | */ 51 | EWMA.prototype.rate = function() { 52 | return this.currentRate * 1000; 53 | } 54 | 55 | EWMA.prototype.stop = function() { 56 | clearInterval(this.tickInterval); 57 | } 58 | 59 | module.exports.createM1EWMA = function(){ return new EWMA(M1_ALPHA, 5000); } 60 | module.exports.createM5EWMA = function(){ return new EWMA(M5_ALPHA, 5000); } 61 | module.exports.createM15EWMA = function(){ return new EWMA(M15_ALPHA, 5000); } 62 | -------------------------------------------------------------------------------- /stats/index.js: -------------------------------------------------------------------------------- 1 | exports.EWMA = require('./exponentially_weighted_moving_average'); 2 | exports.ExponentiallyDecayingSample = require('./exponentially_decaying_sample'); 3 | exports.Sample = require('./sample'); 4 | exports.UniformSample = require('./uniform_sample'); 5 | -------------------------------------------------------------------------------- /stats/sample.js: -------------------------------------------------------------------------------- 1 | var Sample = module.exports = function Sample() { 2 | this.values = []; 3 | this.count = 0; 4 | } 5 | var Sample = module.exports = function Sample() {} 6 | Sample.prototype.init = function(){ this.clear(); } 7 | Sample.prototype.update = function(val){ this.values.push(val); }; 8 | Sample.prototype.clear = function(){ this.values = []; this.count = 0; }; 9 | Sample.prototype.size = function(){ return this.values.length;}; 10 | Sample.prototype.print = function(){console.log(this.values);} 11 | Sample.prototype.getValues = function(){ return this.values; } 12 | 13 | -------------------------------------------------------------------------------- /stats/uniform_sample.js: -------------------------------------------------------------------------------- 1 | var Sample = require('./sample'); 2 | 3 | /* 4 | * Take a uniform sample of size size for all values 5 | */ 6 | var UniformSample = module.exports = function UniformSample(size) { 7 | this.limit = size; 8 | this.count = 0; 9 | this.init(); 10 | } 11 | 12 | UniformSample.prototype = new Sample(); 13 | 14 | UniformSample.prototype.update = function(val) { 15 | this.count++; 16 | if (this.size() < this.limit) { 17 | //console.log("Adding "+val+" to values."); 18 | this.values.push(val); 19 | } else { 20 | var rand = parseInt(Math.random() * this.count); 21 | if (rand < this.limit) { 22 | this.values[rand] = val; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /support/gitdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "metricsjs.net" 3 | } -------------------------------------------------------------------------------- /support/reference/Counter.md: -------------------------------------------------------------------------------- 1 | Counter counts things. 2 | 3 | Counter implements: [`inc()`](#Counter.inc), [`dec()`](#Counter.dec), and [`clear()`](#Counter.clear) 4 | 5 | Counter exposes: [`count`](#Counter.count) 6 | 7 | ### Instantiate 8 | `Counter` is an exported constructor in `metrics`. 9 | 10 | ``` 11 | var metrics = require('metrics'), 12 | counter = new metrics.Counter; 13 | 14 | counter.inc(1); 15 | counter.inc(3); 16 | counter.dec(2); 17 | counter.count; // 2 18 | counter.clear(); 19 | counter.count; // 0 20 | ``` 21 | 22 | ### .inc 23 | `Counter.inc(val)` 24 | 25 | Increment the counter by the given `val`, wrapping the new count if over `4294967296`. 26 | 27 | ### .dec 28 | `Counter.dec(val)` 29 | 30 | Decrement the counter by the given `val`, the resulting count is never less than `0`. 31 | 32 | ### .clear 33 | `Counter.clear()` 34 | 35 | Clear the counter resetting the count to `0`. 36 | 37 | ### .count 38 | `Counter.count` 39 | 40 | Get the current count. 41 | -------------------------------------------------------------------------------- /support/reference/Histogram.md: -------------------------------------------------------------------------------- 1 | Histogram samples a dataset to get a sense of its distribution. These are particularly useful for breaking down the quantiles of how long things take(requests, method calls, etc). Metrics supports uniform distributions and exponentially decaying samples. Sample sizes and parameters of the distribution are all highly configurable, but the defaults may suit your needs. Exponential decay histograms favor more recent data, which is typically what you want. 2 | 3 | Histogram instances implement: [`clear()`](#Histogram.clear), [`update()`](#Histogram.update), [`percentiles()`](#Histogram.percentiles), [`mean()`](#Histogram.mean), and [`stdDev()`](#Histogram.stdDev) 4 | 5 | Histogram instances expose: `count`, `min`, `max`, and `sum` 6 | 7 | Histogram implements: [`createExponentialDecayHistogram()`](#Histogram.createExponentialDecayHistogram) and [`createUniformHistogram()`](#Histogram.createUniformHistogram) 8 | 9 | ### Instantiate 10 | `Histogram` is an exported constructor in `metrics`. It's not typically created by hand though, instead a few functions are given to create certain histograms. 11 | 12 | ``` 13 | var metrics = require('metrics'), 14 | hist = new metrics.Histogram.createUniformHistogram(); 15 | 16 | hist.update(1); 17 | hist.update(3); 18 | hist.mean(); // 2 19 | ``` 20 | 21 | ### .clear 22 | `Histogram.clear()` 23 | 24 | Clear the histogram data. 25 | 26 | ### .update 27 | `Histogram.update(val)` 28 | 29 | Update the sample with the given value. 30 | 31 | ### .percentiles 32 | `Histogram.percentiles(percentiles)` 33 | 34 | Calculate the scores for the samples values from the given percentiles array. 35 | 36 | ### .mean 37 | `Histogram.mean()` 38 | 39 | Get the mean. 40 | 41 | ### .stdDev 42 | `Histogram.stdDev()` 43 | 44 | Get the standard deviation. 45 | 46 | ### .createExponentialDecayHistogram 47 | `Histogram.createExponentialDecayHistogram(size, alpha)` 48 | 49 | Create a new exponential decay histogram using the given size and alpha. 50 | 51 | ### .createUniformHistogram 52 | `Histogram.createUniformHistogram(size)` 53 | 54 | Create a new uniform histogram using the given size. -------------------------------------------------------------------------------- /support/reference/Meter.md: -------------------------------------------------------------------------------- 1 | Meter tracks how often things happen. It exposes a 1 minute, 5 minute, and 15 minute rate using exponentially weighted moving averages(the same strategy that unix load average takes). 2 | 3 | Meter implements: [`mark()`](#Meter.mark), [`oneMinuteRate()`](#Meter.oneMinuteRate), [`fiveMinuteRate()`](#Meter.fiveMinuteRate), [`fifteenMinuteRate()`](#Meter.fifteenMinuteRate), [`meanRate()`](#Meter.meanRate) 4 | 5 | ### Instantiate 6 | `Meter` is an exported constructor in `metrics`. 7 | 8 | ``` 9 | var metrics = require('metrics'), 10 | meter = new metrics.Meter; 11 | 12 | meter.mark(); 13 | meter.mark(); 14 | meter.mark(); 15 | meter.meanRate(); // Depends on how fast marks are called. 16 | ``` 17 | 18 | ### .mark 19 | `Meter.mark(n)` 20 | 21 | Mark the occurence of `n` events 22 | 23 | ### .oneMinuteRate 24 | `Meter.oneMinuteRate()` 25 | 26 | Get the mark rate per second for a minute. 27 | 28 | ### .fiveMinuteRate 29 | `Meter.fiveMinuteRate()` 30 | 31 | Get the mark rate per second for 5 minutes. 32 | 33 | ### .fifteenMinuteRate 34 | `Meter.fifteenMinuteRate()` 35 | 36 | Get the mark rate per second for 15 minutes. 37 | 38 | ### .meanRate 39 | `Meter.meanRate()` 40 | 41 | Get the mean rate. -------------------------------------------------------------------------------- /support/reference/Report.md: -------------------------------------------------------------------------------- 1 | Report tracks multiple metric instruments in a structured way with namespaces. 2 | 3 | Report implements: [`addMetric()`](#Report.addMetric), [`getMetric()`](#Report.getMetric), [`summary()`](#Report.summary) 4 | 5 | ### Instantiate 6 | `Report` is an exported constructor in `metrics`. 7 | 8 | ``` 9 | var metrics = require('metrics'), 10 | report = new metrics.Report(); 11 | 12 | report.addMetric('namespace.name', new metrics.Counter()); 13 | report.addMetric('counter', new metrics.Counter()); 14 | report.summary(); // {...} 15 | ``` 16 | 17 | Metrics can be structured by using namespaces, to put an instrument behind a namespace include the namespace before the metrics name followed by a period(e.g. `namespace.name`). 18 | 19 | You can also include metrics already being tracked when instantiating the report. To do this, when you create an instance give it an object in the format 20 | ``` 21 | { 22 | namespace: { name: new metrics.Counter() }, 23 | '': {counter: new metrics.Counter()} // Items without a namespace 24 | } 25 | ``` 26 | 27 | ### .addMetric 28 | `Report.addMetric(eventName, metric)` 29 | 30 | Add a new metric instrument to the given event name. 31 | 32 | ### .getMetric 33 | `Report.getMetric(eventName)` 34 | 35 | Retrieve the metric for the given event name. 36 | 37 | ### .summary 38 | `Report.summary()` 39 | 40 | Get a summary of the metric instruments being tracked in a readable format. 41 | -------------------------------------------------------------------------------- /support/reference/Server.md: -------------------------------------------------------------------------------- 1 | Server is an HTTP server that prints summaries for a `Report`. 2 | 3 | Report implements: [`addMetric()`](#Server.addMetric) 4 | 5 | ### Instantiate 6 | `Server` is an exported constructor in `metrics`. 7 | 8 | ``` 9 | var metrics = require('metrics'), 10 | server = new metrics.Server(3000); 11 | 12 | server.addMetric('namespace.name', new metrics.Counter()); 13 | server.addMetric('counter', new metrics.Counter()); 14 | ``` 15 | 16 | The server takes a port argument, and optionally a object of metrics to track, for this format please check the `Report` instantiation documentation. 17 | 18 | The server responds with the report summary in JSON format at the endpoint `/metrics`. 19 | 20 | For more advanced usage of reporting, you can use the `Report` constructor and create an HTTP server that responds however you'd like. 21 | 22 | ### .addMetric 23 | `Report.addMetric(eventName, metric)` 24 | 25 | Delegated to the servers Report instance. -------------------------------------------------------------------------------- /support/reference/Timer.md: -------------------------------------------------------------------------------- 1 | Timer is a combination of a `Meter` and a `Histogram`. It samples timing data and rate the data is coming in. Everything you could possibly want! 2 | 3 | Timer implements: [`clear()`](#Timer.clear), [`update()`](#Timer.update), [`mark()`](#Timer.mark), [`count()`](#Timer.count), [`min()`](#Timer.min), [`max()`](#Timer.max), [`mean()`](#Timer.mean), [`stdDev()`](#Timer.stdDev), [`percentiles()`](#Timer.percentiles), [`oneMinuteRate()`](#Timer.oneMinuteRate), [`fiveMinuteRate()`](#Timer.fiveMinuteRate), [`fifteenMinuteRate()`](#Timer.fifteenMinuteRate), [`meanRate()`](#Timer.meanRate) 4 | 5 | ### Instantiate 6 | `Timer` is an exported constructor in `metrics`. 7 | 8 | ``` 9 | var metrics = require('metrics'), 10 | timer = new metrics.Timer; 11 | 12 | timer.update(1); 13 | ``` 14 | 15 | ### .clear 16 | `Timer.clear()` 17 | 18 | Delegated to the timers `Histogram` instance. 19 | 20 | ### .update 21 | `Timer.update(duration)` 22 | 23 | Update the timers duration. 24 | 25 | ### .count 26 | `Timer.count()` 27 | 28 | Delegated to the timers `Histogram` instance. 29 | 30 | ### .min 31 | `Timer.min()` 32 | 33 | Delegated to the timers `Histogram` instance. 34 | 35 | ### .max 36 | `Timer.max()` 37 | 38 | Delegated to the timers `Histogram` instance. 39 | 40 | ### .mean 41 | `Timer.mean()` 42 | 43 | Delegated to the timers `Histogram` instance. 44 | 45 | ### .stdDev 46 | `Timer.stdDev()` 47 | 48 | Delegated to the timers `Histogram` instance. 49 | 50 | ### .percentiles 51 | `Timer.percentiles(percentiles)` 52 | 53 | Delegated to the timers `Histogram` instance. 54 | 55 | ### .oneMinuteRate 56 | `Timer.oneMinuteRate()` 57 | 58 | Delegated to the timers `Meter` instance. 59 | 60 | ### .fiveMinuteRate 61 | `Timer.fiveMinuteRate()` 62 | 63 | Delegated to the timers `Meter` instance. 64 | 65 | ### .fifteenMinuteRate 66 | `Timer.fifteenMinuteRate()` 67 | 68 | Delegated to the timers `Meter` instance. 69 | 70 | ### .meanRate 71 | `Timer.meanRate()` 72 | 73 | Delegated to the timers `Meter` instance. -------------------------------------------------------------------------------- /support/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikejihbe/metrics/bf82b727d080b1b83fade4ac142470c41ab65ff4/support/styles.css -------------------------------------------------------------------------------- /test/unit/cached_gauge.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , CachedGauge = require('../../metrics/cached_gauge'); 5 | 6 | describe('CachedGauge', function() { 7 | function seqFn() { 8 | var i = 0; 9 | return function() { 10 | return i++; 11 | } 12 | } 13 | 14 | it('should call function when asked for a value for the first time.', function() { 15 | var gauge = new CachedGauge(seqFn(), 100); 16 | expect(gauge.printObj()).to.have.property('value', 0); 17 | }); 18 | 19 | it('should not call function before expiration timeout.', function() { 20 | var gauge = new CachedGauge(seqFn(), 1000); 21 | expect(gauge.printObj()).to.have.property('value', 0); 22 | expect(gauge.printObj()).to.have.property('value', 0); 23 | }); 24 | 25 | it('should call function after expiration timeout.', function(done) { 26 | var gauge = new CachedGauge(seqFn(), 50); 27 | expect(gauge.printObj()).to.have.property('value', 0); 28 | setTimeout(function() { 29 | expect(gauge.printObj()).to.have.property('value', 1); 30 | done(); 31 | }, 100); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/console_reporter.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , helper = require('./helper.js') 4 | , it = require('mocha').it 5 | , util = require('util') 6 | , os = require('os') 7 | , ConsoleReporter = require('../../').ConsoleReporter; 8 | 9 | describe('ConsoleReporter', function () { 10 | it ('should report to console.', function () { 11 | // keep track of original log. 12 | var olog = console.log; 13 | 14 | // override console.log to capture lines logged. 15 | var data = []; 16 | console.log = function log() { 17 | var args = Array.prototype.slice.call(arguments); 18 | var msg = util.format.apply(util, args); 19 | data.push(msg); 20 | }; 21 | 22 | // report sample report and capture stdout. 23 | var report = helper.getSampleReport(); 24 | var reporter = new ConsoleReporter(report); 25 | reporter.report(); 26 | 27 | // reset console.log. 28 | console.log = olog; 29 | 30 | // validate line by line. This may be overkill but will detect when 31 | // unanticipated changes affect reported output. 32 | expect(data.length).to.equal(48); 33 | expect(data[0]).to.equal('Counters -----------------------------------------------------------------------'); 34 | expect(data[1]).to.equal('basicCount'); 35 | expect(data[2]).to.equal(' count = 5'); 36 | expect(data[3]).to.equal(''); 37 | expect(data[4]).to.equal('Meters -------------------------------------------------------------------------'); 38 | expect(data[5]).to.equal('myapp.Meter'); 39 | expect(data[6]).to.equal(' count = 10'); 40 | // can't definitively assert the exact mean rate since time elapsed is unknown. 41 | expect(data[7]).to.match(/ mean rate = .* events\/second/); 42 | expect(data[8]).to.equal(' 1-minute rate = 0.00 events/second'); 43 | expect(data[9]).to.equal(' 5-minute rate = 0.00 events/second'); 44 | expect(data[10]).to.equal(' 15-minute rate = 0.00 events/second'); 45 | expect(data[11]).to.equal(''); 46 | expect(data[12]).to.equal('Timers -------------------------------------------------------------------------'); 47 | expect(data[13]).to.equal('myapp.Timer'); 48 | expect(data[14]).to.equal(' count = 100'); 49 | expect(data[15]).to.match(/ mean rate = .* events\/second/); 50 | expect(data[16]).to.equal(' 1-minute rate = 0.00 events/second'); 51 | expect(data[17]).to.equal(' 5-minute rate = 0.00 events/second'); 52 | expect(data[18]).to.equal(' 15-minute rate = 0.00 events/second'); 53 | expect(data[19]).to.equal(' min = 1.00 milliseconds'); 54 | expect(data[20]).to.equal(' max = 100.00 milliseconds'); 55 | expect(data[21]).to.equal(' mean = 50.50 milliseconds'); 56 | expect(data[22]).to.equal(' stddev = 29.01 milliseconds'); 57 | expect(data[23]).to.equal(' 50% <= 50.50 milliseconds'); 58 | expect(data[24]).to.equal(' 75% <= 75.75 milliseconds'); 59 | expect(data[25]).to.equal(' 95% <= 95.95 milliseconds'); 60 | expect(data[26]).to.equal(' 98% <= 98.98 milliseconds'); 61 | expect(data[27]).to.equal(' 99% <= 99.99 milliseconds'); 62 | expect(data[28]).to.equal(' 99.9% <= 100.00 milliseconds'); 63 | expect(data[29]).to.equal(''); 64 | expect(data[30]).to.equal('Histograms ---------------------------------------------------------------------'); 65 | expect(data[31]).to.equal('myapp.Histogram'); 66 | expect(data[32]).to.equal(' count = 100'); 67 | expect(data[33]).to.equal(' min = 2.00'); 68 | expect(data[34]).to.equal(' max = 200.00'); 69 | expect(data[35]).to.equal(' mean = 101.00'); 70 | expect(data[36]).to.equal(' stddev = 58.02'); 71 | expect(data[37]).to.equal(' 50% <= 101.00'); 72 | expect(data[38]).to.equal(' 75% <= 151.50'); 73 | expect(data[39]).to.equal(' 95% <= 191.90'); 74 | expect(data[40]).to.equal(' 98% <= 197.96'); 75 | expect(data[41]).to.equal(' 99% <= 199.98'); 76 | expect(data[42]).to.equal(' 99.9% <= 200.00'); 77 | expect(data[43]).to.equal(''); 78 | expect(data[44]).to.equal('Gauges -------------------------------------------------------------------------'); 79 | expect(data[45]).to.equal('myapp.Gauge'); 80 | expect(data[46]).to.equal(' value = 0.8'); 81 | expect(data[47]).to.equal(''); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/unit/counter.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , Counter = require('../../metrics/counter') 5 | 6 | describe('Counter', function() { 7 | it('inc() should do nothing when 0 is passed', function() { 8 | var counter = new Counter(); 9 | counter.inc(0); 10 | 11 | expect(counter.count).to.equal(0); 12 | }); 13 | it('dec() should do nothing when 0 is passed', function() { 14 | var counter = new Counter(); 15 | counter.dec(0); 16 | 17 | expect(counter.count).to.equal(0); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/csv_reporter.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , assert = chai.assert 3 | , expect = chai.expect 4 | , debug = require('debug')('metrics') 5 | , describe = require('mocha').describe 6 | , fs = require('fs') 7 | , helper = require('./helper.js') 8 | , it = require('mocha').it 9 | , os = require('os') 10 | , path = require('path') 11 | , util = require('util') 12 | , CsvReporter = require('../../').CsvReporter; 13 | 14 | describe('CsvReporter', function () { 15 | this.timeout(5000); 16 | it('should append to files on interval.', function (done) { 17 | var tryCount = 0; 18 | var tmpdir; 19 | do { 20 | var r = 'csv_reporter_test_' + Math.floor((Math.random() * 65535) + 1024); 21 | tmpdir = path.join(os.tmpdir(), r); 22 | } while(fs.existsSync(tmpdir) && tryCount++ < 10); 23 | 24 | if(fs.existsSync(tmpdir)) { 25 | debug("Could not find a tmpdir to create after 10 tries."); 26 | } 27 | debug('Making and using directory %j', tmpdir); 28 | fs.mkdirSync(tmpdir); 29 | 30 | var report = helper.getSampleReport(); 31 | var reporter = new CsvReporter(report, tmpdir); 32 | debug("Starting CsvReporter at 1000ms interval reporting to %s", tmpdir); 33 | reporter.start(1000); 34 | 35 | var counterFile = path.join(tmpdir, 'basicCount.csv'); 36 | var meterFile = path.join(tmpdir, 'myapp.Meter.csv'); 37 | var timerFile = path.join(tmpdir, 'myapp.Timer.csv'); 38 | var histFile = path.join(tmpdir, 'myapp.Histogram.csv'); 39 | var gaugeFile = path.join(tmpdir, 'myapp.Gauge.csv'); 40 | var files = [counterFile, meterFile, timerFile, histFile, gaugeFile]; 41 | 42 | setTimeout(function() { 43 | debug("Reading files %s. Each file should have 1 header and 3 recordings.", files); 44 | reporter.stop(); 45 | files.forEach(function(f) { 46 | if(fs.existsSync(f)) { 47 | debug("Reading file: %s.", f); 48 | var content = fs.readFileSync(f); 49 | debug("File contents:\n%s", content); 50 | var data = content.toString('UTF-8').split('\n'); 51 | expect(data.length).to.equal(5); 52 | // validate headers and content. 53 | if(f === counterFile) { 54 | expect(data[0]).to.equal('t,count'); 55 | data.slice(1, 4).forEach(function (line) { 56 | expect(line).to.match(/.*,5/); 57 | }); 58 | } else if (f === meterFile) { 59 | expect(data[0]).to.equal('t,count,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit'); 60 | data.slice(1, 4).forEach(function (line) { 61 | // don't validate mean rate since we can't determine that explicitly here. 62 | expect(line).to.match(/.*,10,.*,0,0,0,events\/second/); 63 | }); 64 | } else if (f === histFile) { 65 | expect(data[0]).to.equal('t,count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999'); 66 | data.slice(1, 4).forEach(function (line) { 67 | expect(line).to.match(/.*,100,200,101,2,58.02298395176403,101,151.5,191.89999999999998,197.96,199.98,200/); 68 | }); 69 | } else if (f === gaugeFile) { 70 | expect(data[0]).to.equal('t,value'); 71 | data.slice(1, 4).forEach(function (line) { 72 | expect(line).to.match(/.*,0.8/); 73 | }); 74 | } else { 75 | expect(data[0]).to.equal('t,count,max,mean,min,stddev,p50,p75,p95,p98,p99,p999,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit,duration_unit'); 76 | data.slice(1, 4).forEach(function (line) { 77 | expect(line).to.match(/.*,100,100,50.5,1,29.011491975882016,50.5,75.75,95.94999999999999,98.98,99.99,100,.*,0,0,0,calls\/second,millisecond/); 78 | }); 79 | } 80 | // last line should be empty. 81 | expect(data[4]).to.equal(''); 82 | fs.unlinkSync(f); 83 | } else { 84 | assert.fail(false, false, "File " +f + " does not exist!"); 85 | } 86 | }); 87 | debug("Removing dir %j", tmpdir); 88 | fs.rmdirSync(tmpdir); 89 | done(); 90 | }, 3500); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/unit/gauge.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , Gauge = require('../../metrics/gauge'); 5 | 6 | describe('Gauge', function() { 7 | it('should call function every time value is requested.', function() { 8 | var i = 0; 9 | var gauge = new Gauge(function() { return i++; }); 10 | 11 | expect(gauge.printObj()).to.have.property('value', 0); 12 | expect(gauge.printObj()).to.have.property('value', 1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/unit/graphite_reporter.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | , debug = require('debug')('metrics') 3 | , expect = chai.expect 4 | , assert = chai.assert 5 | , describe = require('mocha').describe 6 | , helper = require('./helper.js') 7 | , it = require('mocha').it 8 | , net = require('net') 9 | , util = require('util') 10 | , os = require('os') 11 | , GraphiteReporter = require('../../').GraphiteReporter; 12 | 13 | chai.use(require('chai-string')); 14 | 15 | 16 | describe('GraphiteReporter', function () { 17 | this.timeout(30000); 18 | it ('should report to graphite.', function (done) { 19 | var connectCount = 0; 20 | var reporter; 21 | var dataByTs = {}; 22 | var countOnDisconnect; 23 | var server = net.createServer(function (client) { 24 | debug("Reporter connected."); 25 | client.on('data', function(data) { 26 | debug('Received data:\n%s', data); 27 | var lines = data.toString("UTF-8").split('\n'); 28 | lines.forEach(function(l) { 29 | var d = l.split(' '); 30 | var ts = d[2]; 31 | var nameAndVal = d.slice(0, 2); 32 | if(ts) { 33 | if (!(ts in dataByTs)) { 34 | dataByTs[ts] = []; 35 | } 36 | dataByTs[ts].push(nameAndVal); 37 | } 38 | }); 39 | }); 40 | 41 | connectCount++; 42 | // On first connect, schedule a disconnect in 5 seconds. 43 | if(connectCount == 1) { 44 | setTimeout(function () { 45 | debug("Closing client to see if it reconnects."); 46 | client.destroy(); 47 | countOnDisconnect = Object.keys(dataByTs).length; 48 | }, 5000); 49 | } else { 50 | // On second connect, shutdown in 2.5 secs. 51 | setTimeout(function () { 52 | debug("Shutting down reporter and server."); 53 | if (reporter) { 54 | reporter.stop(); 55 | } 56 | server.close(); 57 | if (typeof callback == 'function') { 58 | callback(); 59 | } 60 | var totalReports = Object.keys(dataByTs).length; 61 | // should expect more reports since disconnected. 62 | expect(totalReports).to.be.greaterThan(countOnDisconnect); 63 | 64 | Object.keys(dataByTs).forEach(function (ts) { 65 | var tsData = dataByTs[ts]; 66 | // Counter should have 1 value. 67 | // Meter should have 5 values. 68 | // Timer should have 15 values. 69 | // Histogram should have 11 values. 70 | // Gauge should have 1 value. 71 | expect(tsData.length).to.equal(33); 72 | // Metric names should start with host name. 73 | tsData.forEach(function (metric) { 74 | expect(metric[0]).to.startsWith('host1.'); 75 | }); 76 | // Timestamp should contain only digits 77 | expect(ts).to.match(/^\d+$/, 'timestamp should be an integer'); 78 | }); 79 | done(); 80 | }, 2500); 81 | } 82 | }); 83 | 84 | server.on('error', function(err) { 85 | assert.fail(false, false, err); 86 | }); 87 | 88 | server.listen(0, "0.0.0.0", function() { 89 | var address = server.address(); 90 | var report = helper.getSampleReport(); 91 | reporter = new GraphiteReporter(report, "host1", address.address, address.port); 92 | reporter.on('log', function(level, msg, exc) { 93 | if(exc) { 94 | debug('%s -- %s (%s)', level, msg, exc); 95 | } else { 96 | debug('%s -- %s', level, msg); 97 | } 98 | }); 99 | reporter.start(1000); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/unit/helper.js: -------------------------------------------------------------------------------- 1 | var metrics = require('../../'), 2 | Report = metrics.Report, 3 | Counter = metrics.Counter, 4 | Timer = metrics.Timer, 5 | Meter = metrics.Meter, 6 | Histogram = metrics.Histogram, 7 | CachedGauge = metrics.CachedGauge, 8 | util = require('util'); 9 | 10 | function getSampleReport() { 11 | var counter = new Counter(); 12 | counter.inc(5); 13 | var timer = new Timer(); 14 | for (var i = 1; i <= 100; i++) { 15 | timer.update(i); 16 | } 17 | var meter = new Meter(); 18 | meter.mark(10); 19 | var hist = new Histogram(); 20 | for (var i = 1; i <= 100; i++) { 21 | hist.update(i*2); 22 | } 23 | var gauge = new CachedGauge(function () { 24 | return 0.8 25 | }, 10000); 26 | var report = new Report(); 27 | report.addMetric("basicCount", counter); 28 | report.addMetric("myapp.Meter", meter); 29 | report.addMetric("myapp.Timer", timer); 30 | report.addMetric("myapp.Histogram", hist); 31 | report.addMetric("myapp.Gauge", gauge); 32 | return report; 33 | } 34 | 35 | exports.getSampleReport = getSampleReport; 36 | -------------------------------------------------------------------------------- /test/unit/histogram.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , Histogram = require('../../metrics/histogram'); 5 | 6 | describe('Histogram', function() { 7 | it('should properly record percentiles from uniform distribution.', function() { 8 | var unifHist = Histogram.createUniformHistogram(5000); 9 | 10 | for(var i = 1; i < 10000; i++) { 11 | unifHist.update(i); 12 | } 13 | 14 | expect(unifHist.mean()).to.equal(5000); 15 | expect(unifHist.variance()).to.equal(8332500); 16 | expect(unifHist.stdDev()).to.equal(2886.607004772212); 17 | expect(unifHist.count).to.equal(9999); 18 | expect(unifHist.min).to.equal(1); 19 | expect(unifHist.max).to.equal(9999); 20 | }); 21 | }); -------------------------------------------------------------------------------- /test/unit/meter.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , Meter = require('../../metrics/meter'); 5 | 6 | describe('Meter', function() { 7 | this.timeout(10000); 8 | it('should properly record rate and count.', function(done) { 9 | var meter = new Meter(); 10 | var updateInterval = setInterval(function() { 11 | meter.mark(1); 12 | }, 100); 13 | 14 | setTimeout(function() { 15 | clearInterval(updateInterval); 16 | expect(meter).to.have.property('count').to.be.within(45,55); 17 | ['mean', '1', '5', '15'].forEach(function(v) { 18 | expect(meter.rates()).to.have.property(v).to.be.within(9,11); 19 | }); 20 | done(); 21 | }, 5000); 22 | }); 23 | }); -------------------------------------------------------------------------------- /test/unit/scheduled_reporter.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , debug = require('debug')('metrics') 3 | , describe = require('mocha').describe 4 | , helper = require('./helper.js') 5 | , it = require('mocha').it 6 | , util = require('util') 7 | , ScheduledReporter = require('../../').ScheduledReporter; 8 | 9 | describe('ScheduledReporter', function () { 10 | this.timeout(20000); 11 | it ('should report on interval.', function (done) { 12 | var invocations = 0; 13 | 14 | function CountingReporter(registry) { 15 | CountingReporter.super_.call(this, registry); 16 | } 17 | util.inherits(CountingReporter, ScheduledReporter); 18 | 19 | CountingReporter.prototype.report = function() { 20 | invocations++; 21 | debug("Invocation #%d", invocations); 22 | }; 23 | 24 | var report = helper.getSampleReport(); 25 | var reporter = new CountingReporter(report); 26 | debug("Starting reporter on 2000ms interval, running for 11000ms."); 27 | reporter.start(2000); 28 | 29 | setTimeout(function() { 30 | expect(invocations).to.equal(5); 31 | debug("Stopping reporter and waiting 3 seconds to ensure report isn't called again."); 32 | reporter.stop(); 33 | setTimeout(function() { 34 | expect(invocations).to.equal(5); 35 | done(); 36 | }, 3000); 37 | }, 11000); 38 | }); 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /test/unit/timer.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , describe = require('mocha').describe 3 | , it = require('mocha').it 4 | , Timer = require('../../metrics/timer'); 5 | 6 | describe('Timer', function() { 7 | it('should properly record durations.', function(done) { 8 | var timer = new Timer(); 9 | 10 | var n = 0; 11 | var updateInterval = setInterval(function() { 12 | timer.update(n++); 13 | }, 100); 14 | 15 | setTimeout(function() { 16 | clearInterval(updateInterval); 17 | timer.tick(); 18 | expect(timer.count()).to.be.within(8, 12); 19 | expect(timer.min()).to.equal(0); 20 | expect(timer.max()).to.be.within(8, 12); 21 | expect(timer).to.have.property('mean'); 22 | expect(timer).to.have.property('stdDev'); 23 | expect(timer).to.have.property('percentiles'); 24 | expect(timer).to.have.property('values'); 25 | expect(timer).to.have.property('oneMinuteRate'); 26 | expect(timer).to.have.property('fiveMinuteRate'); 27 | expect(timer).to.have.property('fifteenMinuteRate'); 28 | expect(timer).to.have.property('meanRate'); 29 | expect(timer).to.have.property('rates'); 30 | done(); 31 | }, 1000); 32 | }); 33 | 34 | it('should record duration using TimerContext.', function(done) { 35 | var timer = new Timer(); 36 | var time = timer.time(); 37 | 38 | var interval = setInterval(function() { 39 | time.stop(); 40 | }, 100); 41 | 42 | setTimeout(function() { 43 | clearInterval(interval); 44 | timer.tick(); 45 | expect(timer.count()).to.be.within(1, 2); 46 | expect(timer.max()).to.be.within(98, 150); 47 | done(); 48 | }, 150); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/uniform_sample.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , debug = require('debug')('metrics') 3 | , describe = require('mocha').describe 4 | , it = require('mocha').it 5 | , UniformSample = require('../../stats/uniform_sample'); 6 | 7 | describe('UniformSample', function () { 8 | it('average of sampled values should fall within acceptable range of actual mean.', function () { 9 | debug("Creating a new UniformSample with a limit of 600."); 10 | var uniformSample = new UniformSample(600); 11 | for (var i = 0; i < 10000; i++) { 12 | uniformSample.update(i); 13 | } 14 | 15 | var sum = 0; 16 | for (var k in uniformSample.values) { 17 | sum += uniformSample.values[k]; 18 | } 19 | var mean = sum / uniformSample.values.length; 20 | debug("Mean of sample is %j", mean); 21 | // actual mean of all 10000 values should be 5000. 22 | expect(mean).to.be.within(4500, 5500); 23 | }); 24 | }); -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | var Report = require('../').Report, 2 | Counter = require('../').Counter, 3 | Timer = require('../').Timer, 4 | Meter = require('../').Meter, 5 | util = require('util'); 6 | 7 | function getSampleReport() { 8 | var counter = new Counter(); 9 | counter.inc(5); 10 | var timer = new Timer(); 11 | for (var i = 1; i <= 100; i++) { 12 | timer.update(i); 13 | } 14 | var meter = new Meter(); 15 | meter.mark(10); 16 | var report = new Report(); 17 | report.addMetric("basicCount", counter); 18 | report.addMetric("myapp.Meter", meter); 19 | report.addMetric("myapp.Timer", timer); 20 | return report; 21 | } 22 | 23 | exports.getSampleReport = getSampleReport; 24 | -------------------------------------------------------------------------------- /tests/test_all.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Returns a function that is the composition of a list of functions, 3 | * each consuming the return value of the function that follows. 4 | */ 5 | var slice = Array.prototype.slice; 6 | var compose = function() { 7 | var funcs = slice.call(arguments); 8 | return function() { 9 | var args = slice.call(arguments); 10 | for (var i=funcs.length-1; i >= 0; i--) { 11 | args = [funcs[i].apply(this, args)]; 12 | } 13 | return args[0]; 14 | }; 15 | }; 16 | 17 | compose( 18 | require('./test_exponentially_weighted_moving_average') 19 | , require('./test_exponentially_decaying_sample') 20 | )(); 21 | -------------------------------------------------------------------------------- /tests/test_exponentially_decaying_sample.js: -------------------------------------------------------------------------------- 1 | var ExponentiallyDecayingSample = require('../stats/exponentially_decaying_sample.js') 2 | , eds = new ExponentiallyDecayingSample(1000, 0.015); 3 | 4 | var test = function(callback) { 5 | eds.clear(); 6 | 7 | var time = (new Date()).getTime() 8 | , interval = 60 * 60 * 1000 / 100; 9 | for(var i = 0; i < 100; i++) { 10 | for(var j = 0; j < 100; j++) { 11 | eds.update(i, time + i * interval); 12 | } 13 | } 14 | 15 | function printSample(eds) { 16 | var valueCounts = {} 17 | , values = eds.getValues(); 18 | 19 | for(var i = 0; i < eds.size(); i++) { 20 | if (valueCounts[values[i].val]) { 21 | valueCounts[values[i].val]++; 22 | } else { 23 | valueCounts[values[i].val] = 1; 24 | } 25 | } 26 | return valueCounts; 27 | } 28 | 29 | console.log("This is an exponential distribution:"); 30 | console.log(printSample(eds)); 31 | 32 | eds.rescale(time + 100*interval); 33 | 34 | console.log("This is after rescaling"); 35 | console.log(eds.getValues()); 36 | if (typeof callback == 'function') { 37 | callback(); 38 | } 39 | } 40 | 41 | if (module.parent) { 42 | module.exports = test; 43 | } else { 44 | test(); 45 | } -------------------------------------------------------------------------------- /tests/test_exponentially_weighted_moving_average.js: -------------------------------------------------------------------------------- 1 | var ExponentiallyWeightedMovingAverage = require('../stats/exponentially_weighted_moving_average'); 2 | var exponentially_weighted_moving_average = new ExponentiallyWeightedMovingAverage.createM1EWMA(); 3 | 4 | var test = function (callback) { 5 | console.log("\nTesting ExponentiallyWeightedMovingAverage\n"); 6 | console.log("Sending updates every 100 milliseconds for 30 seconds."); 7 | 8 | var updateInterval = setInterval(function(){ 9 | exponentially_weighted_moving_average.update(); 10 | }, 100); 11 | setTimeout(function(){ 12 | clearInterval(updateInterval); 13 | console.log("\n\nExpected Average: 10"); 14 | console.log("Exponentially Weighted Moving Average: "+exponentially_weighted_moving_average.rate()+"\n"); 15 | if (typeof callback == 'function') { 16 | callback(); 17 | } 18 | }, 30000); 19 | } 20 | 21 | if (module.parent) { 22 | module.exports = test; 23 | } else { 24 | test(); 25 | } 26 | --------------------------------------------------------------------------------