-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathStat.js
336 lines (313 loc) · 11.4 KB
/
Stat.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/*jslint browser: true */
// Stat.js (c) 2010-2014 Loren West and other contributors
// May be freely distributed under the MIT license.
// For further details and documentation:
// http://lorenwest.github.com/node-monitor
(function(root){
// Module loading
var Monitor = root.Monitor || require('./Monitor'),
// Raw events on the server (for speed), backbone events on the browser (for functionality)
EventEmitter = Monitor.commonJS ? require('events').EventEmitter.prototype : Monitor.Backbone.Events,
_ = Monitor._,
emittingNow = false;
/**
* A lightweight component for gathering and emitting application statistics
*
* This is both a collector and emitter for application stats.
*
* It's designed with low development and runtime cost in mind, encouraging
* usage with minimum concern for overhead.
*
* Stat Collector
* --------------
*
* As a collector, it's a place to send application stats as they're discovered.
*
* Example for incrementing a stat in your application:
*
* var stat = require('monitor').getStatLogger('myModule');
* ...
* stat.increment('requests.inbound');
*
* The above is a request to increment the ```myModule.requests.inbound``` stat.
* It peforms work only if someone is listening for that event.
*
* Stat Emitter
* -------------
* As an emitter, Stat is a place to gather stats as they're collected.
*
* When listening for stats, wildcards can be used to register for many stats
* within a group. For example, the following call:
*
* var Stat = require('monitor').Stat;
* Stat.on('myModule.*.timer', myFunction);
*
* Will call ```myFunction``` when all ```myModule.*.timer``` stats are emitted.
*
* Listeners are invoked with 4 arguments:
*
* - module - The statLogger module name
* - name - The name of the stat that just fired
* - value - The numeric value passed
* - type - An enumeration of the types of stats:<br/>
* 'c' - Counter. Add (or subtract) the value to (or from) the prior value<br/>
* 'g' - Gague. Value is to be recorded as provided<br/>
* 'ms' - Timer. Millisecond amount of time something took.
*
* <h2 id="wildcards">Wildcards</h2>
*
* The following wildcards are allowed for registering events. They're
* modeled after the graphite wildcard syntax (from the
* <a href="https://graphite.readthedocs.org/en/latest/render_api.html#paths-and-wildcards">graphite docs</a>):
*
* #### Delimiter
* The period (.) character is literal, and matches name segment separators.
*
* #### Asterisk
* The asterisk (*) matches zero or more characters. It is non-greedy, so you
* can have more than one within a single path element.
*
* Example: servers.ix\*ehssvc\*v.cpu.total.\* will return all total CPU metrics
* for all servers matching the given name pattern.
*
* An asterisk at the far right of the pattern matches everything to the right,
* including all path segments. For example, ```servers.*``` matches all
* names beginning with ```servers.```.
*
* #### Character list or range
* Characters in square brackets ([...]) specify a single character position in
* the path string, and match if the character in that position matches one of
* the characters in the list or range.
*
* A character range is indicated by 2 characters separated by a dash (-), and
* means that any character between those 2 characters (inclusive) will match.
* More than one range can be included within the square brackets, e.g. foo[a-z0-9]bar
* will match foopbar, foo7bar etc..
*
* If the characters cannot be read as a range, they are treated as a
* list - any character in the list will match, e.g. foo[bc]ar will match
* foobar and foocar. If you want to include a dash (-) in your list, put
* it at the beginning or end, so it's not interpreted as a range.
*
* #### Value list
* Comma-separated values within curly braces ({foo,bar,...}) are treated as
* value lists, and match if any of the values matches the current point in
* the path. For example, servers.ix01ehssvc04v.cpu.total.{user,system,iowait}
* will match the user, system and I/O wait total CPU metrics for the specified
* server.
*
* #### Javascript Regex
* For finer grained expression matching, a javascript style regex can be
* specified using the ```/.../``` syntax. This style spans the entire identifier.
* You can ignore case using the ```/.../i``` syntax. If the first character of the
* string is a slash, it considers the string a javascript regular expression.
*
* Choosing Good Names
* -------------------
* It's a good idea to pick a good naming scheme with each dot-delimited segment
* having a consistent, well-defined purpose. Volatile segments should be as deep
* into the hierarchy (furthest right) as possible. Keeping the names less
* volatile makes it easier to turn recording on for all statistics.
*
* @class Stat
* @constructor
*/
var Stat = Monitor.Stat = function(module) {
var t = this;
t.module = module;
};
var proto = Stat.prototype;
// This is a map of registered event names to compiled regexs, for
// quickly testing if a statistic needs to be emitted.
Stat.eventRegex = {};
/**
* Increment a counter by a specified value
*
* Assuming someone is listening to this stat, this is an instruction for that
* listener to add the specified value (usually 1) to their prior value for this stat.
*
* This is known as server-side setting, as the server (listener) is responsible
* for maintaining the prior and new value for the stat.
*
* @method increment
* @param name {String} Dot.separated name of the counter to increment
* @param [value=1] {Number} Amount to increment the counter by.
*/
proto.increment = function(name, value){
value = _.isNumber(value) ? value : 1;
Stat._emit(this.module, name, value, 'c');
};
/**
* Decrement a counter by a specified value
*
* Assuming someone is listening to this stat, this is an instruction for that
* listener to subtract the specified value (usually 1) to their prior value for this stat.
*
* This is known as server-side setting, as the server (listener) is responsible
* for maintaining the prior and new value for the stat.
*
* @method decrement
* @param name {String} Dot.separated name of the counter to decrement
* @param [value=1] {Number} Amount to decrement the counter by.
*/
proto.decrement = function(name, value){
value = _.isNumber(value) ? value : 1;
Stat._emit(this.module, name, value * -1, 'c');
};
/**
* Set the stat to the specified value
*
* This is an instruction to any (all) listener(s) to set the stat to a
* specific value.
*
* This is known as client-side setting, because the client determines the value
* of the stat.
*
* @method gauge
* @param name {String} Dot.separated name of the stat
* @param value {Number} Number to set the gauge to
*/
proto.gauge = function(name, value){
Stat._emit(this.module, name, value, 'g');
};
/**
* Record the specified duration (in milliseconds) for the stat
*
* This is like Stat.gauge() in that it is a client-side setting of a
* specified value. The difference is the scale of the value is specified
* as milliseconds.
*
* This may be one of the most widely used stat methods. It can (should?) be
* used upon callback from asynchronous methods.
*
* Pattern:
*
* var stat = require('monitor').getStatLogger('myModule');
* ...
* var stamp = Date.now();
* SomeAsyncFunction(arg1, function(error) {
* stat.time('SomeAsyncFunction.time', Date.Now() - stamp);
* ...continue with error handling & callback handling
* });
*
* @method time
* @param name {String} Dot.separated name of the stat
* @param duration {Integer} Number of milliseconds this stat took to complete
*/
proto.time = function(name, duration){
Stat._emit(this.module, name, duration, 'ms');
};
/**
* Send the stat to all registered listeners
*
* @private
* @static
* @method emit
* @param module {String} Module name
* @param name {String} Stat name
* @param value {Numeric} Stat value
* @param type {String} Enumeration. One of the following:
* 'c' - Counter. + values increment, - values decrement
* 'g' - Gague. Statistic is recorded as provided
* 'ms' - Timer. Millisecond amount of time something took
*/
Stat._emit = function(module, name, value, type) {
var eventName,
fullName;
// Prevent stat recursion. This has the effect of disabling all stats
// for stat handlers (and their downstream effect), but is necessary to
// prevent infinite recursion. If it's desired to stat the output of
// stat handlers, then delay that processing until nextTick.
if (emittingNow) {
return;
}
emittingNow = true;
// Test the name against all registered events
for (eventName in Stat._events) {
// Build the full name only if someone is listening
if (!fullName) {
fullName = module + '.' + name;
}
// Get the regex associated with the name
var regex = Stat.eventRegex[eventName];
if (!regex) {
regex = Stat.eventRegex[eventName] = Stat._buildRegex(eventName);
}
// Test the name with the regex, and emit if it matches
if (regex.test(fullName)) {
Stat.emit(eventName, module, name, value, type);
}
}
// Turn off recursion prevention
emittingNow = false;
};
/**
* Build a regex from a user entered string following the pattern described
* in the class definition. Loosely:
*
* If it looks like a JS regexp, process it as a regexp
* Change all '.' to '\.'
* Change all '*' to '[^\.]*' (unless it's at the end, then convert to '.*')
* Change all {one,two} to (one|two)
* Leave all [...] alone - they work as-is
*
* If an error occurs, throw an exception
*
* @private
* @static
* @method _buildRegex
* @param str {String} String to build the regular expression from
* @return {RegExp}The regular expression object
*
*/
Stat._buildRegex = function(str) {
var regexStr = '',
modifier = '',
lastIdx = str.length - 1,
inSquiggly = false;
// Javascript regular expressions
if (/^\/[^\/]*\/i*$/.test(str)) {
if (/i$/.test(str)) {
modifier = 'i';
str = str.replace(/i$/,'');
}
regexStr = '^' + str.replace(/^\//,'').replace(/\/$/,'') + '$';
}
// Process character by character
else {
for (var i = 0, l = str.length; i < l; i++) {
var c = str.substr(i,1);
switch (c) {
case '.':
c = '\\.';
break;
case '*':
c = (i === lastIdx ? '.*' : '[^\\.]*');
break;
case '{':
c = '(';
inSquiggly = true;
break;
case '}':
c = ')';
inSquiggly = false;
break;
case ',':
if (inSquiggly) {
c = '|';
}
break;
}
regexStr += c;
}
// Force it to match the full string
regexStr = '^' + regexStr + '$';
}
// Now build the regex. This throws an exception if poorly formed.
return new RegExp(regexStr, modifier);
};
// Mixin event processing for the Stat class
_.extend(Stat, EventEmitter);
// Expose this class from the Monitor module
Monitor.setStatLoggerClass(Stat);
}(this));