diff --git a/config.js b/config.js index 90a97ec0c..9abb6d7f3 100644 --- a/config.js +++ b/config.js @@ -42,6 +42,7 @@ module.exports = { // This field is experimental. plugins: { 'connect': path.join(__dirname, 'src/plugins/plugin-connect.js'), + 'express': path.join(__dirname, 'src/plugins/plugin-express.js'), 'grpc': path.join(__dirname, 'src/plugins/plugin-grpc.js'), 'hapi': path.join(__dirname, 'src/plugins/plugin-hapi.js'), 'http': path.join(__dirname, 'src/plugins/plugin-http.js'), diff --git a/src/hooks/index.js b/src/hooks/index.js index 55e98e4e6..9265244b4 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -36,10 +36,7 @@ var fs = require('fs'); // will result in the module itself being patched. // Note: the order in which filenames are defined in the hooks determines the // order in which they are loaded. -var toInstrument = Object.create(null, { - 'express': { enumerable: true, value: { file: './userspace/hook-express.js', - patches: {} } } -}); +var toInstrument = Object.create(null); var logger; diff --git a/src/hooks/userspace/hook-express.js b/src/hooks/userspace/hook-express.js deleted file mode 100644 index db32dea15..000000000 --- a/src/hooks/userspace/hook-express.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright 2015 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -var cls = require('../../cls.js'); -var TraceLabels = require('../../trace-labels.js'); -var shimmer = require('shimmer'); -var semver = require('semver'); -var methods = require('methods').concat('use', 'route', 'param', 'all'); -var constants = require('../../constants.js'); -var agent; - -var SUPPORTED_VERSIONS = '4.x'; - -function applicationActionWrap(method) { - return function expressActionTrace() { - if (!this._google_trace_patched && !this._router) { - this._google_trace_patched = true; - this.use(middleware); - } - return method.apply(this, arguments); - }; -} - -function middleware(req, res, next) { - var namespace = cls.getNamespace(); - if (!namespace) { - agent.logger.info('Express: no namespace found, ignoring request'); - return next(); - } - var traceHeader = agent.parseContextFromHeader( - req.get(constants.TRACE_CONTEXT_HEADER_NAME)) || {}; - if (!agent.shouldTrace(req.originalUrl, traceHeader.options)) { - return next(); - } - - namespace.bindEmitter(req); - namespace.bindEmitter(res); - - var originalEnd = res.end; - - namespace.run(function() { - var rootContext = startRootSpanForRequest(req, traceHeader); - var context = agent.generateTraceContext(rootContext, true); - if (context) { - res.set(constants.TRACE_CONTEXT_HEADER_NAME, context); - } else { - agent.logger.warn('express: Attempted to generate trace context for nullSpan'); - } - - // wrap end - res.end = function(chunk, encoding) { - res.end = originalEnd; - var returned = res.end(chunk, encoding); - - endRootSpanForRequest(rootContext, req, res); - return returned; - }; - - next(); - }); -} - -/** - * Creates and sets up a new root span for the given request. - * @param {Object} req The request being processed. - * @param {Object} traceHeader The incoming trace header. - * @returns {!SpanData} The new initialized trace span data instance. - */ -function startRootSpanForRequest(req, traceHeader) { - var traceId = traceHeader.traceId; - var parentSpanId = traceHeader.spanId; - var url = req.protocol + '://' + req.hostname + req.originalUrl; - - // we use the path part of the url as the span name and add the full - // url as a label - var rootContext = agent.createRootSpanData(req.path, traceId, - parentSpanId, 3); - rootContext.addLabel(TraceLabels.HTTP_METHOD_LABEL_KEY, req.method); - rootContext.addLabel(TraceLabels.HTTP_URL_LABEL_KEY, url); - rootContext.addLabel(TraceLabels.HTTP_SOURCE_IP, req.connection.remoteAddress); - return rootContext; -} - - -/** - * Ends the root span for the given request. - * @param {!SpanData} rootContext The span to close out. - * @param {Object} req The request being processed. - * @param {Object} res The response being processed. - */ -function endRootSpanForRequest(rootContext, req, res) { - if (req.route && req.route.path) { - rootContext.addLabel( - 'express/request.route.path', req.route.path); - } - rootContext.addLabel( - TraceLabels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); - rootContext.close(); -} - -module.exports = function(version_, agent_) { - if (!semver.satisfies(version_, SUPPORTED_VERSIONS)) { - agent_.logger.info('Express: unsupported version ' + version_ + ' loaded'); - return {}; - } - return { - // An empty relative path here matches the root module being loaded. - '': { - patch: function(express) { - agent = agent_; - methods.forEach(function(method) { - shimmer.wrap(express.application, method, applicationActionWrap); - }); - }, - unpatch: function(express) { - methods.forEach(function(method) { - shimmer.unwrap(express.application, method); - }); - agent_.logger.info('Express: unpatched'); - } - } - }; -}; diff --git a/src/plugins/plugin-express.js b/src/plugins/plugin-express.js new file mode 100644 index 000000000..08857f2e7 --- /dev/null +++ b/src/plugins/plugin-express.js @@ -0,0 +1,93 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +var shimmer = require('shimmer'); +var methods = require('methods').concat('use', 'route', 'param', 'all'); + +var SUPPORTED_VERSIONS = '4.x'; + +function patchModuleRoot(express, api) { + var labels = api.labels; + function applicationActionWrap(method) { + return function expressActionTrace() { + if (!this._google_trace_patched && !this._router) { + this._google_trace_patched = true; + this.use(middleware); + } + return method.apply(this, arguments); + }; + } + + function middleware(req, res, next) { + var options = { + name: req.path, + traceContext: req.get(api.constants.TRACE_CONTEXT_HEADER_NAME), + url: req.originalUrl, + skipFrames: 3 + }; + api.runInRootSpan(options, function(rootSpan) { + if (!rootSpan) { + next(); + return; + } + + // Set outgoing trace context. + res.set(api.constants.TRACE_CONTEXT_HEADER_NAME, + rootSpan.getTraceContext()); + + api.wrapEmitter(req); + api.wrapEmitter(res); + + var url = req.protocol + '://' + req.hostname + req.originalUrl; + rootSpan.addLabel(labels.HTTP_METHOD_LABEL_KEY, req.method); + rootSpan.addLabel(labels.HTTP_URL_LABEL_KEY, url); + rootSpan.addLabel(labels.HTTP_SOURCE_IP, req.connection.remoteAddress); + + // wrap end + var originalEnd = res.end; + res.end = function(chunk, encoding) { + res.end = originalEnd; + var returned = res.end(chunk, encoding); + + if (req.route && req.route.path) { + rootSpan.addLabel('express/request.route.path', req.route.path); + } + rootSpan.addLabel(labels.HTTP_RESPONSE_CODE_LABEL_KEY, res.statusCode); + rootSpan.endSpan(); + return returned; + }; + + next(); + }); + } + + methods.forEach(function(method) { + shimmer.wrap(express.application, method, applicationActionWrap); + }); +} + +function unpatchModuleRoot(express) { + methods.forEach(function(method) { + shimmer.unwrap(express.application, method); + }); +} + +module.exports = [{ + versions: SUPPORTED_VERSIONS, + patch: patchModuleRoot, + unpatch: unpatchModuleRoot +}];