-
Notifications
You must be signed in to change notification settings - Fork 32
Add InfluxDB Line Protocol Support, and Add endpoint for JSON #18
base: master
Are you sure you want to change the base?
Changes from 5 commits
4988652
8dd8122
8256267
c12964e
fe2d812
fde87e8
d9024e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,9 +57,9 @@ to `/APP_ROOT/v1/send` on whichever port you specify. | |
```bash | ||
# Install nodejs | ||
# This assumes you're on a 64 bit machine | ||
wget http://nodejs.org/dist/v0.10.19/node-v0.10.19-linux-x64.tar.gz | ||
tar xvf node-v0.10.19-linux-x64.tar.gz | ||
sudo ln -s `pwd`/node-v0.10.19-linux-x64/bin/{node,npm} /usr/local/bin/ | ||
wget https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | ||
tar xvf node-v4.1.1-linux-x64.tar.gz | ||
sudo ln -s `pwd`/node-v4.1.1-linux-x64/bin/{node,npm} /usr/local/bin/ | ||
|
||
# Grab a Bucky release | ||
# You should use the latest release available at https://github.com/HubSpot/BuckyServer/releases | ||
|
@@ -101,7 +101,14 @@ If you need more customization, you can write a module: | |
|
||
There are a few of types of modules: | ||
|
||
|
||
- Server - Use to set properties of the Bucky Server | ||
- port - Use to set the port that Bucky Server will listen to | ||
- appRoot - Use to define the root of the endpoint | ||
- https - Use to set options for running Bucky Server in https mode | ||
- port - Use to specify the port for https, if not populated the http server port + 1 | ||
- options - Use to define the certificates for https. [List of all possible options](https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) | ||
- For all options that accept a buffer you can use the path to the file and they'll be read in | ||
- httpsOnly - If this flag is set to true then Bucky Server will not run in http mode | ||
- Logger - Use to have Bucky log to something other than the console | ||
- Config - Use to have Bucky pull config from somewhere other than the default file | ||
- App - Use to do things when Bucky loads and/or on requests. Auth, monitoring initialization, etc. | ||
|
@@ -180,7 +187,7 @@ called with like this: | |
You are free to implement the `on` method as a dud if live reloading doesn't | ||
make sense using your config system. Take a look at [lib/configWrapper.coffee](lib/configWrapper.coffee) | ||
for an example of how a basic object can be converted (and feel free to use it). | ||
|
||
#### App | ||
|
||
App modules get loaded once, and can optionally provide a function to be ran with each request. | ||
|
@@ -236,7 +243,10 @@ module.exports = ({app, logger, config}, next) -> | |
|
||
### Format | ||
|
||
If you are interested in writing new clients, the format of metric data is the same as is used by statsd: | ||
If you are interested in writing new clients, there are two endpoints for inbound data. | ||
The default endpoint uses the same format as statsd: | ||
|
||
default endpoint: `{hostname}:{port}/v1/{appRoot}` uses | ||
|
||
``` | ||
<metric name>:<metric value>|<unit>[@<sample rate>] | ||
|
@@ -249,4 +259,24 @@ my.awesome.metric:35|ms | |
some.other.metric:3|[email protected] | ||
``` | ||
|
||
All requests are sent with content-type `text/plain`. | ||
All post reqeusts sent to the default endpoint must use content-type `text/plain`. | ||
|
||
JSON endpoint: `{hostname}:{port}/v1/{appRoot}/json` uses | ||
|
||
```javascript | ||
{ | ||
"<metric name>": "<metric value>[|<unit>[@<sample rate>]" | ||
} | ||
``` | ||
|
||
This allows for the ':' character to be included in your metrics. This is valid for InfluxDB Line Protocol. | ||
|
||
For example: | ||
```javascript | ||
{ | ||
"page,browser=Chrome,browserVersion=44,url=http://localhost:3000/#customHash/%7Bexample%3A%22encoded%20data%22%7D,key=domContentLoadedEventEnd": "500|ms", | ||
"ajax,browser=Microsoft\\ Internet\\ Explorer,browserVersion=8,url=http://localhost:3000/#customHash/%7Bexample%3A%22encoded%20data%22%7D,endpoint=your/awesome/template.html,method=GET,status=200": "1|c" | ||
} | ||
``` | ||
|
||
All post request to the json endpoint will be *converted* to content-type 'application/json'. This allows for backwards compatibility with IE8 which can't send XDomainRequest with a content-type other than 'plain/text'. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,15 @@ | ||
server: | ||
port: 5999 | ||
appRoot: "/bucky" | ||
# If https options are set then bucky will also listen on https | ||
# https: | ||
# port: 5599 | ||
# See full list of options at: https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener | ||
# options: | ||
# key: "ssl/key.pem" | ||
# cert: "ssl/cert.pem" | ||
# If the following is set to true then bucky server will not run in http mode | ||
# httpsOnly: true | ||
|
||
statsd: | ||
host: 'localhost' | ||
|
@@ -18,7 +27,7 @@ influxdb: | |
password: 'root' | ||
use_udp: false | ||
retentionPolicy: 'default' | ||
# Acceptable version are: '0.8' and '0.9' | ||
# All version other than '0.9' will default to '0.8' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is kind of odd. It should probably error if you try to enter a value other than '0.8' or '0.9' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for backwards compatibility. If a user updates (or re-downloads) BuckyServer and tries to use an old default.yaml the current defaulting will handle it gracefully, and BuckyServer will act as it did before the version option was added. If this is changed to throw an error then older default.yaml configurations for InfluxDB will become invalid. Unless we add logic to only allow passed values to be '0.8', '0.9', and default undefined to '0.8'. I'll work on that last option. |
||
version: '0.9' | ||
|
||
|
||
|
@@ -32,7 +41,7 @@ modules: | |
|
||
collectors: | ||
# Uncomment the modules that you'd like to use | ||
# - ./modules/collectionLogger | ||
- ./modules/collectionLogger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason for turning this on by default? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the last PR I sent you I commented out all of the collectors because the log showed errors when trying to connect to databases that weren't set up. I'd have to double check but I believe I also received errors when no collectors were available. I thought it would be best to have the logger for the initial setup so that end-users won't receive errors, and they'll also be able to verify that the endpoint is being reached before adding database config. I can comment this out again if you'd like. |
||
# - ./modules/statsd | ||
# - ./modules/openTSDB | ||
# - ./modules/influxdb |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,95 @@ | ||
request = require('request') | ||
dgram = require('dgram') | ||
request = require "request" | ||
dgram = require "dgram" | ||
|
||
class Client | ||
constructor: (@config={}, @logger) -> | ||
do @init | ||
|
||
init: -> | ||
useUDP = @config.get('influxdb.use_udp').get() ? false | ||
useUDP = @config.get("influxdb.use_udp").get() ? false | ||
|
||
@send = if useUDP then @sendUDP() else @sendHTTP() | ||
|
||
write: (metrics) -> | ||
@send @metricsJson metrics | ||
@send @formatMetrics metrics | ||
|
||
sendHTTP: -> | ||
version = @config.get('influxdb.version').get() ? '0.9' | ||
host = @config.get('influxdb.host').get() ? 'localhost' | ||
port = @config.get('influxdb.port').get() ? 8086 | ||
database = @config.get('influxdb.database').get() ? 'bucky' | ||
username = @config.get('influxdb.username').get() ? 'root' | ||
password = @config.get('influxdb.password').get() ? 'root' | ||
version = @config.get("influxdb.version").get() ? "0.9" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why double quotes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There a mix between single and double quotes, I wanted to make the quotes more uniform, I just picked one over the other. If you'd like it changed back just let me know. Really there's no other reason (silly article): http://www.2ality.com/2012/09/javascript-quotes.html There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed these back to single quotes. |
||
host = @config.get("influxdb.host").get() ? "localhost" | ||
port = @config.get("influxdb.port").get() ? 8086 | ||
database = @config.get("influxdb.database").get() ? "bucky" | ||
username = @config.get("influxdb.username").get() ? "root" | ||
password = @config.get("influxdb.password").get() ? "root" | ||
retentionPolicy = @config.get("influxdb.retentionPolicy").get() ? "default" | ||
logger = @logger | ||
if version == '0.8' | ||
endpoint = 'http://' + host + ':' + port + '/db/' + database + '/series' | ||
else | ||
endpoint = 'http://' + host + ':' + port + '/write' | ||
client = request.defaults | ||
method: 'POST' | ||
url: endpoint | ||
|
||
clientConfig = | ||
method: "POST" | ||
qs: | ||
u: username | ||
p: password | ||
|
||
(metricsJson) -> | ||
client form: metricsJson, (error, response, body) -> | ||
logger.log error if error | ||
if version == "0.9" | ||
clientConfig.url = "http://" + host + ":" + port + "/write" | ||
clientConfig.qs.db = database | ||
clientConfig.qs.rp = retentionPolicy | ||
else | ||
clientConfig.url = "http://" + host + ":" + port + "/db/" + database + "/series" | ||
|
||
client = request.defaults clientConfig | ||
|
||
(formatMetrics) -> | ||
if version == "0.9" | ||
metrics = formatMetrics.join "\n" | ||
# uncomment to see data sent to DB | ||
# logger.log "db: " + database + "\n" + metrics | ||
client body: metrics, (error, response, body) -> | ||
logger.log "Warning:" if body && body.length > 0 | ||
logger.log "\tresponse:\n", body if body && body.length > 0 | ||
logger.log error if error | ||
else | ||
metrics = JSON.stringify formatMetrics | ||
# logger.log "db: " + database + "\n" + metrics | ||
client form: metrics, (error, response, body) -> | ||
logger.log "Warning:" if body && body.length > 0 | ||
logger.log "\tresponse:\n", body if body && body.length > 0 | ||
logger.log error if error | ||
|
||
sendUDP: -> | ||
host = @config.get('influxdb.host').get() ? 'localhost' | ||
port = @config.get('influxdb.port').get() ? 4444 | ||
client = dgram.createSocket 'udp4' | ||
|
||
(metricsJson) -> | ||
message = new Buffer metricsJson | ||
version = @config.get("influxdb.version").get() ? "0.9" | ||
host = @config.get("influxdb.host").get() ? "localhost" | ||
port = @config.get("influxdb.port").get() ? 4444 | ||
client = dgram.createSocket "udp4" | ||
|
||
(formatMetrics) -> | ||
if version == "0.9" | ||
formatMetrics.forEach (metric) -> | ||
message = new Buffer metric | ||
client.send message, 0, message.length, port, host | ||
else | ||
message = new Buffer JSON.stringify formatMetrics | ||
client.send message, 0, message.length, port, host | ||
|
||
client.send message, 0, message.length, port, host | ||
formatMetrics: (metrics) -> | ||
version = @config.get("influxdb.version").get() ? "0.9" | ||
data = [] | ||
|
||
metricsJson: (metrics) -> | ||
version = @config.get('influxdb.version').get() ? '0.9' | ||
if version == '0.8' | ||
data = [] | ||
else | ||
data = | ||
database: @config.get('influxdb.database').get() ? 'bucky' | ||
retentionPolicy: @config.get('influxdb.retentionPolicy').get() ? "default" | ||
time: new Date().toISOString() | ||
points: [] | ||
for key, desc of metrics | ||
[val, unit, sample] = @parseRow desc | ||
|
||
if version == '0.8' | ||
data.push | ||
if version == "0.9" | ||
fields = key.replace(/\\? /g, "\\ ") | ||
fields += " value=" + parseFloat val | ||
fields += ',unit="' + unit.replace(/"/g, '\\"') + '"' if unit | ||
fields += ",sample=" + sample if sample | ||
else | ||
fields = | ||
name: key, | ||
columns: ['value'], | ||
columns: ["value"], | ||
points: [[parseFloat val]] | ||
else | ||
data.points.push | ||
measurement: key | ||
fields: | ||
value: parseFloat val | ||
unit: unit | ||
sample: sample | ||
# @logger.log(JSON.stringify(data, null, 2)) | ||
JSON.stringify data | ||
data.push fields | ||
|
||
data | ||
|
||
parseRow: (row) -> | ||
re = /([0-9\.]+)\|([a-z]+)(?:@([0-9\.]+))?/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,10 @@ | |
Q = require 'q' | ||
_ = require 'underscore' | ||
express = require 'express' | ||
http = require 'http' | ||
|
||
# Set cwd for config, and load config file | ||
process.chdir(__dirname); | ||
process.chdir __dirname | ||
config = require 'config' | ||
|
||
configWrapper = require './lib/configWrapper' | ||
|
@@ -14,23 +15,29 @@ load = require './lib/load' | |
MODULES = config.modules | ||
loadLogger = -> | ||
if MODULES.logger | ||
load(MODULES.logger, {config}) | ||
load MODULES.logger, {config} | ||
else | ||
console | ||
|
||
# We always have the base config, but the | ||
# app can optionally swap it out for something else. | ||
loadConfig = (logger) -> | ||
if MODULES.config | ||
load(MODULES.config, {config, logger}) | ||
load MODULES.config, {config, logger} | ||
else | ||
configWrapper(config) | ||
configWrapper config | ||
|
||
setCORSHeaders = (req, res, next) -> | ||
res.setHeader 'Access-Control-Allow-Origin', '*' | ||
res.setHeader 'Access-Control-Allow-Methods', 'POST' | ||
res.setHeader 'Access-Control-Max-Age', '604800' | ||
res.setHeader 'Access-Control-Allow-Credentials', 'true' | ||
res.setHeader 'Access-Control-Allow-Headers', 'content-type' | ||
|
||
next() | ||
|
||
setJSONHeader = (req, res, next) -> | ||
req.headers['content-type'] = 'application/json' | ||
|
||
next() | ||
|
||
|
@@ -103,6 +110,7 @@ loadApp = (logger, loadedConfig) -> | |
|
||
for path, handlers of routes | ||
# Bind all request modules as middleware and install the collectors | ||
app.post "#{ path }/json", setJSONHeader, express.json(), setCORSHeaders, handlers... | ||
app.post path, parser, setCORSHeaders, handlers... | ||
|
||
app.options path, setCORSHeaders, (req, res) -> | ||
|
@@ -111,15 +119,29 @@ loadApp = (logger, loadedConfig) -> | |
app.get "#{ APP_ROOT }/v1/health-check", (req, res) -> | ||
res.send('OK\n') | ||
|
||
port = process.env.PORT ? loadedConfig.get('server.port').get() ? 5000 | ||
app.listen(port) | ||
|
||
logger.log('Server listening on port %d in %s mode', port, app.settings.env) | ||
if loadedConfig.get('server.https.options').get() instanceof Object | ||
https = require 'https' | ||
fs = require 'fs' | ||
httpsOptions = _.mapObject loadedConfig.get('server.https.options').get(), (v, k) -> | ||
if _.isString(v) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is saying any option can also be a file? That seems very dangerous. What if my string value also happens to be a file? I'm not a big fan of errors which appear and disappear based on what files show up in the local directory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most https certs are stored in files. The https server requires either the entire cert string or a buffer. With this logic it checks to see if the file exists then we load the contents of the file into a buffer, or keep the original string. By I added an ssl/ path to the example in default.yaml to help prevent the issue you're talking about. I don't believe any of the https options take a forward-slash as part of the option string (unless it was an entire cert string, which wouldn't match a file path). This is a shortcut around listing each and every type of option that can be a file buffer. If you'd prefer to have them spelled out I can do that. Here are all of the available options: Some additional documentation on express/connect/https
Note: Another option would be to replace default.yaml with a custom config module that returns an object. That way we could require 'fs' and allow end-users to add the options similar to the options in the nodejitsu link above and specify when the option should be a file buffer instead of a string. This would obviously not be backwards compatible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reviewed this with a co-worker and he agreed with you. Even though it's unlikely an option would share its value with a file, it's not necessary to introduce that risk. He gave one suggestion for keeping the yaml format, which would be to pass an object containing a specified key, such as filePath, with the path. Example: server:
port: 5999
appRoot: "/bucky"
https:
port: 5599
options:
key:
filePath: "ssl/key.pem"
cert:
filePath: "ssl/cert.pem" Then to change the logic to check for an object with key "filePath" instead of a string before attempting to read in a file. I'll implement this and update the README |
||
try | ||
fs.readFileSync(v) | ||
catch | ||
v | ||
else | ||
v | ||
httpsPort = loadedConfig.get('server.https.port').get() ? (port + 1) | ||
https.createServer(httpsOptions, app).listen httpsPort | ||
logger.log "HTTPS Server listening on port %d in %s mode", httpsPort, app.settings.env | ||
if !loadedConfig.get('server.httpsOnly').get() | ||
port = process.env.PORT ? loadedConfig.get('server.port').get() ? 5000 | ||
http.createServer(app).listen port | ||
logger.log 'HTTP Server listening on port %d in %s mode', port, app.settings.env | ||
|
||
Q.when(loadLogger()).then (logger) -> | ||
|
||
logger.log "Loading Config" | ||
Q.when(loadConfig(logger)).then (loadedConfig) -> | ||
Q.when(loadConfig logger).then (loadedConfig) -> | ||
|
||
logger.log "Loading App" | ||
loadApp(logger, loadedConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is describing a new type of module. Where is that module defined and used in the source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm rewriting this now. I must have gotten interrupted initially when I was writing out the configuration option details and clearly mixed up the config options with modules. Thanks for catching this.