Skip to content

Commit

Permalink
feat(tracing): Support MySQL
Browse files Browse the repository at this point in the history
  • Loading branch information
bripkens committed May 9, 2017
1 parent dac8fda commit a8eedd8
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 3 deletions.
10 changes: 8 additions & 2 deletions .travis.yml
Expand Up @@ -18,8 +18,9 @@ node_js:
- v7

before_script:
- docker-compose up -d
- sleep 10
- sudo /etc/init.d/mysql stop
- docker-compose kill && docker-compose rm -f && docker-compose up -d
- sleep 45

after_failure:
- docker ps
Expand All @@ -31,6 +32,11 @@ env:
- MONGODB=127.0.0.1:27017
- ZOOKEEPER=127.0.0.1:2181
- KAFKA=127.0.0.1:9092
- MYSQL_HOST=127.0.0.1
- MYSQL_PORT=3306
- MYSQL_USER=root
- MYSQL_PW=nodepw
- MYSQL_DB=nodedb
- CXX=g++-4.8

notifications:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog

## Unreleased
- Support tracing for the mysql module.

## 1.24.0
- Collect healthcheck results.

Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Expand Up @@ -27,5 +27,10 @@ export MONGODB="127.0.0.1:27017"
export ELASTICSEARCH="127.0.0.1:9200"
export ZOOKEEPER="127.0.0.1:2181"
export KAFKA="127.0.0.1:9092"
export MYSQL_HOST="127.0.0.1"
export MYSQL_PORT="3306"
export MYSQL_USER="root"
export MYSQL_PW="nodepw"
export MYSQL_DB="nodedb"
npm test
```
13 changes: 13 additions & 0 deletions docker-compose.yml
Expand Up @@ -25,3 +25,16 @@ services:
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
volumes:
- /var/run/docker.sock:/var/run/docker.sock

mysql:
image: mysql:8.0.1
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: nodepw
MYSQL_DATABASE: nodedb
MYSQL_USER: node
MYSQL_PASSWORD: nodepw
MYSQL_ROOT_HOST: 0.0.0.0
volumes:
- ./test/mysqlConfig:/etc/mysql/conf.d
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -18,7 +18,8 @@
"dev-http-proxy": "APP_PORT=3457 UPSTREAM_PORT=3456 TRACING_ENABLED=true node test/apps/expressProxy | bunyan",
"dev-http-elasticsearch": "APP_PORT=3458 TRACING_ENABLED=true node test/apps/expressElasticsearch | bunyan",
"dev-http-mongodb": "APP_PORT=3459 TRACING_ENABLED=true node test/apps/expressMongodb | bunyan",
"dev-http-kafka-producer": "APP_PORT=3460 TRACING_ENABLED=true node test/apps/expressKafkaProducer | bunyan"
"dev-http-kafka-producer": "APP_PORT=3460 TRACING_ENABLED=true node test/apps/expressKafkaProducer | bunyan",
"dev-http-mysql": "APP_PORT=3461 TRACING_ENABLED=true node test/apps/expressMysql | bunyan"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -65,6 +66,7 @@
"mocha": "2.3.3",
"mongodb": "2.2.9",
"morgan": "^1.8.1",
"mysql": "^2.13.0",
"proxyquire": "1.7.3",
"request": "2.74.0",
"request-promise": "4.1.1",
Expand Down
4 changes: 4 additions & 0 deletions src/tracing/hook.js
Expand Up @@ -64,6 +64,10 @@ exports.postAndDestroySimulated = function(uid) {
delete handleData[uid];
};

exports.isUidExisting = function isUidExisting(uid) {
return uid in handleData;
};

exports.preAsync = function pre(uid) {
active = uid;
};
Expand Down
3 changes: 3 additions & 0 deletions src/tracing/index.js
Expand Up @@ -19,6 +19,7 @@ exports.init = function(config) {
require('./instrumentation/elasticsearch.js').init(config);
require('./instrumentation/mongodb.js').init(config);
require('./instrumentation/kafka.js').init(config);
require('./instrumentation/mysql.js').init(config);
}
};

Expand Down Expand Up @@ -70,6 +71,7 @@ exports.activate = function() {
require('./instrumentation/elasticsearch.js').activate();
require('./instrumentation/mongodb.js').activate();
require('./instrumentation/kafka.js').activate();
require('./instrumentation/mysql.js').activate();
require('./opentracing').activate();
};

Expand All @@ -80,6 +82,7 @@ exports.deactivate = function() {
}

require('./opentracing').deactivate();
require('./instrumentation/mysql.js').deactivate();
require('./instrumentation/kafka.js').deactivate();
require('./instrumentation/mongodb.js').deactivate();
require('./instrumentation/elasticsearch.js').deactivate();
Expand Down
151 changes: 151 additions & 0 deletions src/tracing/instrumentation/mysql.js
@@ -0,0 +1,151 @@
'use strict';

var shimmer = require('shimmer');

var requireHook = require('../../util/requireHook');
var transmission = require('../transmission');
var tracingUtil = require('../tracingUtil');
var hook = require('../hook');

var isActive = false;

exports.init = function() {
requireHook.on('mysql', instrument);
};


function instrument(mysql) {
var Connection = Object.getPrototypeOf(mysql.createConnection({}));
var Pool = Object.getPrototypeOf(mysql.createPool({}));
shimmer.wrap(Connection, 'query', shimQuery);
shimmer.wrap(Pool, 'query', shimQuery);
shimmer.wrap(Pool, 'getConnection', shimGetConnection);
}


function shimQuery(original) {
return function() {
if (isActive) {
return instrumentedQuery(this, original, arguments[0], arguments[1], arguments[2]);
}
return original.apply(this, arguments);
};
}


function instrumentedQuery(ctx, originalQuery, statementOrOpts, valuesOrCallback, optCallback) {
var argsForOriginalQuery = [statementOrOpts, valuesOrCallback];
if (typeof optCallback !== 'undefined') {
argsForOriginalQuery.push(optCallback);
}

var uid = hook.initAndPreSimulated();
var tracingSuppressed = hook.isTracingSuppressed(uid);
if (tracingSuppressed || hook.containsExitSpan(uid)) {
return originalQuery.apply(ctx, argsForOriginalQuery);
}

var host;
var port;
var user;
var db;
if (ctx.config) {
if (ctx.config.connectionConfig) {
host = ctx.config.connectionConfig.host;
port = ctx.config.connectionConfig.port;
user = ctx.config.connectionConfig.user;
db = ctx.config.connectionConfig.database;
} else {
host = ctx.config.host;
port = ctx.config.port;
user = ctx.config.user;
db = ctx.config.database;
}
}

hook.markAsExitSpan(uid);

var spanId = tracingUtil.generateRandomSpanId();
var traceId = hook.getTraceId(uid);
var parentId = undefined;
if (!traceId) {
traceId = spanId;
} else {
parentId = hook.getParentSpanId(uid);
}

var span = {
s: spanId,
t: traceId,
p: parentId,
f: tracingUtil.getFrom(),
async: false,
error: false,
ec: 0,
ts: Date.now(),
d: 0,
n: 'mysql',
b: {
s: 1
},
stack: tracingUtil.getStackTrace(instrumentedQuery),
data: {
mysql: {
stmt: typeof statementOrOpts === 'string' ? statementOrOpts : statementOrOpts.sql,
host: host,
port: port,
user: user,
db: db
}
}
};
hook.setSpanId(uid, span.s);

var originalCallback = argsForOriginalQuery[argsForOriginalQuery.length - 1];
argsForOriginalQuery[argsForOriginalQuery.length - 1] = function onQueryResult(error) {
if (error) {
span.ec = 1;
span.error = true;
span.data.mysql.error = error.message;
}

span.d = Date.now() - span.ts;
transmission.addSpan(span);
hook.postAndDestroySimulated(uid);

if (originalCallback) {
return originalCallback.apply(this, arguments);
}
};

return originalQuery.apply(ctx, argsForOriginalQuery);
}

function shimGetConnection(original) {
return function(cb) {
var targetContextUid = hook.getCurrentUid();
return original.call(this, wrappedCallback);

function wrappedCallback() {
if (hook.isUidExisting(targetContextUid)) {
var originalContextUid = hook.getCurrentUid();
hook.preAsync(targetContextUid);
hook.initAndPreSimulated();
var result = cb.apply(this, arguments);
hook.preAsync(originalContextUid);
return result;
}

return cb.apply(this, arguments);
}
};
}

exports.activate = function() {
isActive = true;
};


exports.deactivate = function() {
isActive = false;
};
102 changes: 102 additions & 0 deletions test/apps/expressMysql.js
@@ -0,0 +1,102 @@
/* eslint-disable no-console */

'use strict';

require('../../')({
agentPort: process.env.AGENT_PORT,
level: 'info',
tracing: {
enabled: process.env.TRACING_ENABLED === 'true',
forceTransmissionStartingAt: 1
}
});

var mysql = require('mysql');
var bodyParser = require('body-parser');
var express = require('express');
var morgan = require('morgan');

var app = express();
var logPrefix = 'Express / MySQL App (' + process.pid + '):\t';
var pool = mysql.createPool({
connectionLimit: 5,
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PW,
database: process.env.MYSQL_DB
});

pool.getConnection(function(err, connection) {
if (err) {
log('Failed to get connection for table creation', err);
return;
}

connection.query('CREATE TABLE random_values (value double);', function(queryError) {
connection.release();

if (queryError && queryError.code !== 'ER_TABLE_EXISTS_ERROR') {
log('Failed to execute query for table creation', queryError);
return;
}

log('Successfully created table');
});
});

if (process.env.WITH_STDOUT) {
app.use(morgan(logPrefix + ':method :url :status'));
}

app.use(bodyParser.json());


app.get('/', function(req, res) {
res.sendStatus(200);
});

app.get('/values', function(req, res) {
pool.query('SELECT value FROM random_values', function(queryError, results) {
if (queryError) {
log('Failed to execute query', queryError);
res.sendStatus(500);
return;
}

res.json(results.map(function(result) {
return result.value;
}));
});
});

app.post('/values', function(req, res) {
pool.getConnection(function(err, connection) {
if (err) {
log('Failed to get connection', err);
res.sendStatus(500);
return;
}

connection.query('INSERT INTO random_values (value) VALUES (?)', [req.query.value], function(queryError) {
connection.release();

if (queryError) {
log('Failed to execute query', queryError);
res.sendStatus(500);
return;
}

res.sendStatus(200);
});
});
});

app.listen(process.env.APP_PORT, function() {
log('Listening on port: ' + process.env.APP_PORT);
});

function log() {
var args = Array.prototype.slice.call(arguments);
args[0] = logPrefix + args[0];
console.log.apply(console, args);
}

0 comments on commit a8eedd8

Please sign in to comment.