You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2020/12/08 03:23:39 UTC

[skywalking-nodejs] branch master updated: Express plugin, make spans actually work (#9)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-nodejs.git


The following commit(s) were added to refs/heads/master by this push:
     new eb26b21  Express plugin, make spans actually work (#9)
eb26b21 is described below

commit eb26b218735ee64935d5708fc527ba92403d9290
Author: Tomasz Pytel <to...@gmail.com>
AuthorDate: Tue Dec 8 00:09:32 2020 -0300

    Express plugin, make spans actually work (#9)
    
    * [WIP] express plugin, make spans actually work
    
    * [Fix] Swapped inject() and extract()
    
    * [Plugin] Express plugin working
    
    * added express component value
    
    * [Plugin]  added https support to HttpPlugin
    
    * [Enhancement] added ignore suffix and path
    
    * [Plugin] axios
---
 src/agent/Buffer.ts                   |   4 +-
 src/config/AgentConfig.ts             |  25 ++++-
 src/core/PluginInstaller.ts           |  37 ++++---
 src/index.ts                          |   3 +-
 src/plugins/AxiosPlugin.ts            | 121 +++++++++++++++++++++++
 src/plugins/ExpressPlugin.ts          | 103 ++++++++++++++++++++
 src/plugins/HttpPlugin.ts             | 177 ++++++++++++++++++----------------
 src/trace/Component.ts                |   1 +
 src/trace/context/SpanContext.ts      |  96 +++++++++++++-----
 src/trace/span/DummySpan.ts           |  11 ++-
 src/trace/span/EntrySpan.ts           |   4 +-
 src/trace/span/ExitSpan.ts            |   2 +-
 src/trace/span/Span.ts                |   9 +-
 src/trace/span/StackedSpan.ts         |   7 +-
 tests/plugins/http/expected.data.yaml |   4 +-
 15 files changed, 464 insertions(+), 140 deletions(-)

diff --git a/src/agent/Buffer.ts b/src/agent/Buffer.ts
index a1cda3b..8f4f061 100644
--- a/src/agent/Buffer.ts
+++ b/src/agent/Buffer.ts
@@ -49,6 +49,4 @@ class Buffer {
   }
 }
 
-export default new Buffer(
-  Number.isSafeInteger(config.maxBufferSize) ? Number.parseInt(config.maxBufferSize, 10) : 1000,
-);
+export default new Buffer(config.maxBufferSize);
diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts
index 9b5a66e..cdae60f 100644
--- a/src/config/AgentConfig.ts
+++ b/src/config/AgentConfig.ts
@@ -25,8 +25,27 @@ export type AgentConfig = {
   collectorAddress?: string;
   authorization?: string;
   maxBufferSize?: number;
+  ignoreSuffix?: string;
+  traceIgnorePath?: string;
+  // the following is internal state computed from config values
+  reIgnoreOperation?: RegExp;
 };
 
+export function finalizeConfig(config: AgentConfig): void {
+  const escapeRegExp = (s: string) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
+
+  const ignoreSuffix =`^.+(?:${config.ignoreSuffix!.split(',').map(s => escapeRegExp(s.trim())).join('|')})$`;
+  const ignorePath = '^(?:' + config.traceIgnorePath!.split(',').map(
+    s1 => s1.trim().split('**').map(
+      s2 => s2.split('*').map(
+        s3 => s3.split('?').map(escapeRegExp).join('[^/]')  // replaces "**"
+      ).join('[^/]*')                                       // replaces "*"
+    ).join('(?:(?:[^/]+\.)*[^/]+)?')                        // replaces "?"
+  ).join('|') + ')$';                                       // replaces ","
+
+  config.reIgnoreOperation = RegExp(`${ignoreSuffix}|${ignorePath}`);
+}
+
 export default {
   serviceName: process.env.SW_AGENT_NAME || 'your-nodejs-service',
   serviceInstance:
@@ -36,5 +55,9 @@ export default {
     })(),
   collectorAddress: process.env.SW_AGENT_COLLECTOR_BACKEND_SERVICES || '127.0.0.1:11800',
   authorization: process.env.SW_AGENT_AUTHENTICATION,
-  maxBufferSize: process.env.SW_AGENT_MAX_BUFFER_SIZE || '1000',
+  maxBufferSize: Number.isSafeInteger(process.env.SW_AGENT_MAX_BUFFER_SIZE) ?
+    Number.parseInt(process.env.SW_AGENT_MAX_BUFFER_SIZE as string, 10) : 1000,
+  ignoreSuffix: process.env.SW_IGNORE_SUFFIX ?? '.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg',
+  traceIgnorePath: process.env.SW_TRACE_IGNORE_PATH || '',
+  reIgnoreOperation: RegExp(''),  // temporary placeholder so Typescript doesn't throw a fit
 };
diff --git a/src/core/PluginInstaller.ts b/src/core/PluginInstaller.ts
index 887cd6f..80ce569 100644
--- a/src/core/PluginInstaller.ts
+++ b/src/core/PluginInstaller.ts
@@ -28,16 +28,16 @@ const logger = createLogger(__filename);
 let topModule = module;
 for (; topModule.parent; topModule = topModule.parent);
 
-const topResolve = (request: string) => (module.constructor as any)._resolveFilename(request, topModule)
-
 class PluginInstaller {
   pluginDir: string;
+  require: (name: string) => any = topModule.require.bind(topModule);
+  resolve = (request: string) => (module.constructor as any)._resolveFilename(request, topModule);
 
   constructor() {
     this.pluginDir = path.resolve(__dirname, '..', 'plugins');
   }
 
-  private isBuiltIn = (module: string): boolean => topResolve(module) === module;
+  private isBuiltIn = (module: string): boolean => this.resolve(module) === module;
 
   private checkModuleVersion = (plugin: SwPlugin): { version: string; isSupported: boolean } => {
     try {
@@ -54,8 +54,8 @@ class PluginInstaller {
       };
     }
 
-    const packageJsonPath = topResolve(`${plugin.module}/package.json`);
-    const version = topModule.require(packageJsonPath).version;
+    const packageJsonPath = this.resolve(`${plugin.module}/package.json`);
+    const version = this.require(packageJsonPath).version;
 
     if (!semver.satisfies(version, plugin.versions)) {
       logger.info(`Plugin ${plugin.module} ${version} doesn't satisfy the supported version ${plugin.versions}`);
@@ -74,18 +74,29 @@ class PluginInstaller {
     fs.readdirSync(this.pluginDir)
       .filter((file) => !(file.endsWith('.d.ts') || file.endsWith('.js.map')))
       .forEach((file) => {
-        const plugin = require(path.join(this.pluginDir, file)).default as SwPlugin;
+        let plugin;
+        const pluginFile = path.join(this.pluginDir, file);
 
-        const { isSupported, version } = this.checkModuleVersion(plugin);
+        try {
+          plugin = require(pluginFile).default as SwPlugin;
+          const { isSupported, version } = this.checkModuleVersion(plugin);
 
-        if (!isSupported) {
-          logger.info(`Plugin ${plugin.module} ${version} doesn't satisfy the supported version ${plugin.versions}`);
-          return;
-        }
+          if (!isSupported) {
+            logger.info(`Plugin ${plugin.module} ${version} doesn't satisfy the supported version ${plugin.versions}`);
+            return;
+          }
 
-        logger.info(`Installing plugin ${plugin.module} ${plugin.versions}`);
+          logger.info(`Installing plugin ${plugin.module} ${plugin.versions}`);
 
-        plugin.install();
+          plugin.install();
+
+        } catch (e) {
+          if (plugin) {
+            logger.error(`Error installing plugin ${plugin.module} ${plugin.versions}`);
+          } else {
+            logger.error(`Error processing plugin ${pluginFile}`);
+          }
+        }
       });
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 87c35cd..698e22e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,7 +17,7 @@
  *
  */
 
-import config, { AgentConfig } from './config/AgentConfig';
+import config, { AgentConfig, finalizeConfig } from './config/AgentConfig';
 import GrpcProtocol from './agent/protocol/grpc/GrpcProtocol';
 import { createLogger } from './logging';
 import Protocol from './agent/protocol/Protocol';
@@ -36,6 +36,7 @@ class Agent {
     }
 
     Object.assign(config, options);
+    finalizeConfig(config);
 
     if (this.started) {
       throw new Error('SkyWalking agent is already started and can only be started once.');
diff --git a/src/plugins/AxiosPlugin.ts b/src/plugins/AxiosPlugin.ts
new file mode 100644
index 0000000..09ff877
--- /dev/null
+++ b/src/plugins/AxiosPlugin.ts
@@ -0,0 +1,121 @@
+/*!
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import SwPlugin from '../core/SwPlugin';
+import { URL } from 'url';
+import ContextManager from '../trace/context/ContextManager';
+import { Component } from '../trace/Component';
+import Span from '../trace/span/Span';
+import Tag from '../Tag';
+import { SpanLayer } from '../proto/language-agent/Tracing_pb';
+import { createLogger } from '../logging';
+import PluginInstaller from '../core/PluginInstaller';
+
+const logger = createLogger(__filename);
+
+class AxiosPlugin implements SwPlugin {
+  readonly module = 'axios';
+  readonly versions = '*';
+  axios = PluginInstaller.require('axios').default;
+
+  install(): void {
+    if (logger.isDebugEnabled()) {
+      logger.debug('installing axios plugin');
+    }
+    this.interceptClientRequest();
+  }
+
+  private interceptClientRequest() {
+    const copyStatusAndStop = (span: Span, response: any) => {
+      if (response) {
+        if (response.status) {
+          span.tag(Tag.httpStatusCode(response.status));
+        }
+        if (response.statusText) {
+          span.tag(Tag.httpStatusMsg(response.statusText));
+        }
+      }
+
+      span.stop();
+    }
+
+    this.axios.interceptors.request.use(
+      (config: any) => {
+        config.span.resync();
+
+        (config.span as Span).inject().items.forEach((item) => {
+          config.headers.common[item.key] = item.value;
+        });
+
+        return config;
+      },
+
+      (error: any) => {
+        error.config.span.error(error);
+        error.config.span.stop();
+
+        return Promise.reject(error);
+      }
+    );
+
+    this.axios.interceptors.response.use(
+      (response: any) => {
+        copyStatusAndStop(response.config.span, response);
+
+        return response;
+      },
+
+      (error: any) => {
+        error.config.span.error(error);
+
+        copyStatusAndStop(error.config.span, error.response);
+
+        return Promise.reject(error);
+      }
+    );
+
+    const _request = this.axios.Axios.prototype.request;
+
+    this.axios.Axios.prototype.request = function (config: any) {
+      const { host, pathname: operation } = new URL(config.url);  // TODO: this may throw invalid URL
+      const span = ContextManager.current.newExitSpan(operation, host).start();
+
+      try {
+        span.component = Component.UNKNOWN;
+        span.layer = SpanLayer.HTTP;
+        span.peer = host;
+        span.tag(Tag.httpURL(host + operation));
+        span.async();
+
+        const request = _request.call(this, {...config, span});
+
+        return request;
+
+      } catch (e) {
+        span.error(e);
+        span.stop();
+
+        throw e;
+      }
+    };
+  }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default new AxiosPlugin();
diff --git a/src/plugins/ExpressPlugin.ts b/src/plugins/ExpressPlugin.ts
new file mode 100644
index 0000000..1cd3172
--- /dev/null
+++ b/src/plugins/ExpressPlugin.ts
@@ -0,0 +1,103 @@
+/*!
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ *
+ */
+
+import SwPlugin from '../core/SwPlugin';
+import { IncomingMessage, ServerResponse } from 'http';
+import ContextManager from '../trace/context/ContextManager';
+import { Component } from '../trace/Component';
+import Tag from '../Tag';
+import { SpanLayer } from '../proto/language-agent/Tracing_pb';
+import { ContextCarrier } from '../trace/context/ContextCarrier';
+import { createLogger } from '../logging';
+import PluginInstaller from '../core/PluginInstaller';
+
+const logger = createLogger(__filename);
+
+class ExpressPlugin implements SwPlugin {
+  readonly module = 'express';
+  readonly versions = '*';
+
+  install(): void {
+    if (logger.isDebugEnabled()) {
+      logger.debug('installing express plugin');
+    }
+    this.interceptServerRequest();
+  }
+
+  private interceptServerRequest() {
+    const onFinished = PluginInstaller.require('on-finished');
+    const router = PluginInstaller.require('express/lib/router');
+    const _handle = router.handle;
+
+    router.handle = function(req: IncomingMessage, res: ServerResponse, out: any) {
+      const headers = req.rawHeaders || [];
+      const headersMap: { [key: string]: string } = {};
+
+      for (let i = 0; i < headers.length / 2; i += 2) {
+        headersMap[headers[i]] = headers[i + 1];
+      }
+
+      const carrier = ContextCarrier.from(headersMap);
+      const operation = (req.url || '/').replace(/\?.*/g, '');
+      const span = ContextManager.current.newEntrySpan(operation, carrier).start();
+
+      let stopped = 0;
+      const stopIfNotStopped = () => {
+        if (!stopped++) {
+          span.stop();
+          span.tag(Tag.httpStatusCode(res.statusCode));
+          if (res.statusCode && res.statusCode >= 400) {
+            span.errored = true;
+          }
+          if (res.statusMessage) {
+            span.tag(Tag.httpStatusMsg(res.statusMessage));
+          }
+        }
+      };
+
+      try {
+        span.layer = SpanLayer.HTTP;
+        span.component = Component.EXPRESS;
+        span.peer = req.headers.host || '';
+        span.tag(Tag.httpURL(span.peer + req.url));
+
+        const ret = _handle.call(this, req, res, (err: Error) => {
+          if (err)
+            span.error(err);
+          else
+            span.errored = true;
+          out.call(this, err);
+          stopped -= 1;  // skip first stop attempt, make sure stop executes once status code and message is set
+          onFinished(req, stopIfNotStopped);  // this must run after any handlers deferred in 'out'
+        });
+        onFinished(req, stopIfNotStopped);
+
+        return ret;
+
+      } catch (e) {
+        span.error(e);
+        stopIfNotStopped();
+        throw e;
+      }
+    };
+  }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default new ExpressPlugin();
diff --git a/src/plugins/HttpPlugin.ts b/src/plugins/HttpPlugin.ts
index 0a1f1db..b237c8b 100644
--- a/src/plugins/HttpPlugin.ts
+++ b/src/plugins/HttpPlugin.ts
@@ -23,6 +23,7 @@ import { ClientRequest, IncomingMessage, RequestOptions, ServerResponse } from '
 import ContextManager from '../trace/context/ContextManager';
 import { Component } from '../trace/Component';
 import Tag from '../Tag';
+import ExitSpan from '../trace/span/ExitSpan';
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
 import { ContextCarrier } from '../trace/context/ContextCarrier';
 import { createLogger } from '../logging';
@@ -37,117 +38,123 @@ class HttpPlugin implements SwPlugin {
     if (logger.isDebugEnabled()) {
       logger.debug('installing http plugin');
     }
-    this.interceptClientRequest();
-    this.interceptServerRequest();
-  }
 
-  private interceptClientRequest() {
     const http = require('http');
+    const https = require('https');
+
+    this.interceptClientRequest(http);
+    this.interceptServerRequest(http);
+    this.interceptClientRequest(https);
+    this.interceptServerRequest(https);
+  }
 
-    ((original) => {
-      http.request = function () {
-        const url: URL | string | RequestOptions = arguments[0];
-
-        const { host, pathname } =
-          url instanceof URL
-            ? url
-            : typeof url === 'string'
-            ? new URL(url)
-            : {
-                host: (url.host || url.hostname || 'unknown') + ':' + url.port,
-                pathname: url.path || '/',
-              };
-        const operation = pathname.replace(/\?.*$/g, '');
-
-        let stopped = 0;  // compensating if request aborted right after creation 'close' is not emitted
-        const span = ContextManager.current.newExitSpan(operation, host).start();
-        const stopIfNotStopped = () => !stopped++ ? span.stop() : null;  // make sure we stop only once
-
-        try {
+  private interceptClientRequest(module: any) {
+    const _request = module.request;
+
+    module.request = function () {
+      const url: URL | string | RequestOptions = arguments[0];
+
+      const { host, pathname } =
+        url instanceof URL
+          ? url
+          : typeof url === 'string'
+          ? new URL(url)  // TODO: this may throw invalid URL
+          : {
+              host: (url.host || url.hostname || 'unknown') + ':' + url.port,
+              pathname: url.path || '/',
+            };
+      const operation = pathname.replace(/\?.*$/g, '');
+
+      let stopped = 0;  // compensating if request aborted right after creation 'close' is not emitted
+      const stopIfNotStopped = () => !stopped++ ? span.stop() : null;  // make sure we stop only once
+      const span: ExitSpan = ContextManager.current.newExitSpan(operation, host).start() as ExitSpan;
+
+      try {
+        if (span.depth === 1) {
           span.component = Component.HTTP;
           span.layer = SpanLayer.HTTP;
+          span.peer = host;
           span.tag(Tag.httpURL(host + pathname));
+        }
 
-          const request: ClientRequest = original.apply(this, arguments);
-
-          span.extract().items.forEach((item) => {
-            request.setHeader(item.key, item.value);
-          });
+        const request: ClientRequest = _request.apply(this, arguments);
 
-          request.on('close', stopIfNotStopped);
-          request.on('abort', () => (span.errored = true, stopIfNotStopped()));
-          request.on('error', (err) => (span.error(err), stopIfNotStopped()));
+        span.inject().items.forEach((item) => {
+          request.setHeader(item.key, item.value);
+        });
 
-          request.prependListener('response', (res) => {
-            span.resync();
-            span.tag(Tag.httpStatusCode(res.statusCode));
-            if (res.statusCode && res.statusCode >= 400) {
-              span.errored = true;
-            }
-            if (res.statusMessage) {
-              span.tag(Tag.httpStatusMsg(res.statusMessage));
-            }
-            stopIfNotStopped();
-          });
+        request.on('close', stopIfNotStopped);
+        request.on('abort', () => (span.errored = true, stopIfNotStopped()));
+        request.on('error', (err) => (span.error(err), stopIfNotStopped()));
 
-          span.async();
+        request.prependListener('response', (res) => {
+          span.resync();
+          span.tag(Tag.httpStatusCode(res.statusCode));
+          if (res.statusCode && res.statusCode >= 400) {
+            span.errored = true;
+          }
+          if (res.statusMessage) {
+            span.tag(Tag.httpStatusMsg(res.statusMessage));
+          }
+          stopIfNotStopped();
+        });
 
-          return request;
+        span.async();
 
-        } catch (e) {
-          if (!stopped) {
-            span.error(e);
-            stopIfNotStopped();
-          }
+        return request;
 
-          throw e;
+      } catch (e) {
+        if (!stopped) {
+          span.error(e);
+          stopIfNotStopped();
         }
-      };
-    })(http.request);
+
+        throw e;
+      }
+    };
   }
 
-  private interceptServerRequest() {
-    const http = require('http');
+  private interceptServerRequest(module: any) {
+    const _emit = module.Server.prototype.emit;
 
-    ((original) => {
-      http.Server.prototype.emit = function () {
-        if (arguments[0] !== 'request') {
-          return original.apply(this, arguments);
-        }
+    module.Server.prototype.emit = function () {
+      if (arguments[0] !== 'request') {
+        return _emit.apply(this, arguments);
+      }
 
-        const [req, res] = [arguments[1] as IncomingMessage, arguments[2] as ServerResponse];
+      const [req, res] = [arguments[1] as IncomingMessage, arguments[2] as ServerResponse];
 
-        const headers = req.rawHeaders || [];
-        const headersMap: { [key: string]: string } = {};
+      const headers = req.rawHeaders || [];
+      const headersMap: { [key: string]: string } = {};
 
-        for (let i = 0; i < headers.length / 2; i += 2) {
-          headersMap[headers[i]] = headers[i + 1];
-        }
+      for (let i = 0; i < headers.length / 2; i += 2) {
+        headersMap[headers[i]] = headers[i + 1];
+      }
 
-        const carrier = ContextCarrier.from(headersMap);
-        const operation = (req.url || '/').replace(/\?.*/g, '');
-        const span = ContextManager.current.newEntrySpan(operation, carrier);
+      const carrier = ContextCarrier.from(headersMap);
+      const operation = (req.url || '/').replace(/\?.*/g, '');
+      const span = ContextManager.current.newEntrySpan(operation, carrier);
 
-        return ContextManager.withSpan(span, (self, args) => {
-          span.component = Component.HTTP_SERVER;
-          span.layer = SpanLayer.HTTP;
-          span.tag(Tag.httpURL((req.headers.host || '') + req.url));
+      return ContextManager.withSpan(span, (self, args) => {
+        span.component = Component.HTTP_SERVER;
+        span.layer = SpanLayer.HTTP;
+        span.peer = req.headers.host || '';
+        span.tag(Tag.httpURL(span.peer + req.url));
 
-          const ret = original.apply(self, args);
+        const ret = _emit.apply(self, args);
 
-          span.tag(Tag.httpStatusCode(res.statusCode));
-          if (res.statusCode && res.statusCode >= 400) {
-            span.errored = true;
-          }
-          if (res.statusMessage) {
-            span.tag(Tag.httpStatusMsg(res.statusMessage));
-          }
+        span.tag(Tag.httpStatusCode(res.statusCode));
+        if (res.statusCode && res.statusCode >= 400) {
+          span.errored = true;
+        }
+        if (res.statusMessage) {
+          span.tag(Tag.httpStatusMsg(res.statusMessage));
+        }
 
-          return ret;
+        return ret;
 
-        }, this, arguments);
-      };
-    })(http.Server.prototype.emit);
+      }, this, arguments);
+    };
   }
 }
 
diff --git a/src/trace/Component.ts b/src/trace/Component.ts
index 54b61c4..60c4c5d 100644
--- a/src/trace/Component.ts
+++ b/src/trace/Component.ts
@@ -22,6 +22,7 @@ export class Component {
   static HTTP = new Component(2);
   static MONGODB = new Component(9);
   static HTTP_SERVER = new Component(49);
+  static EXPRESS = new Component(4002);
 
   constructor(public id: number) {}
 }
diff --git a/src/trace/context/SpanContext.ts b/src/trace/context/SpanContext.ts
index fab8305..f1daaa3 100644
--- a/src/trace/context/SpanContext.ts
+++ b/src/trace/context/SpanContext.ts
@@ -17,8 +17,11 @@
  *
  */
 
+import config from '../../config/AgentConfig';
 import Context from '../../trace/context/Context';
+import DummyContext from '../../trace/context/DummyContext';
 import Span from '../../trace/span/Span';
+import DummySpan from '../../trace/span/DummySpan';
 import Segment from '../../trace/context/Segment';
 import EntrySpan from '../../trace/span/EntrySpan';
 import ExitSpan from '../../trace/span/ExitSpan';
@@ -30,6 +33,7 @@ import Snapshot from '../../trace/context/Snapshot';
 import SegmentRef from '../../trace/context/SegmentRef';
 import { ContextCarrier } from './ContextCarrier';
 import ContextManager from './ContextManager';
+import { SpanType } from '../../proto/language-agent/Tracing_pb';
 
 const logger = createLogger(__filename);
 
@@ -49,6 +53,18 @@ export default class SpanContext implements Context {
     return this.parent ? this.parent.id : -1;
   }
 
+  ignoreCheck(operation: string, type: SpanType): Span | undefined {
+    if (operation.match(config.reIgnoreOperation)) {
+      return new DummySpan({
+        context: new DummyContext(),
+        operation: '',
+        type,
+      });
+    }
+
+    return undefined;
+  }
+
   newEntrySpan(operation: string, carrier?: ContextCarrier): Span {
     if (logger.isDebugEnabled()) {
       logger.debug('Creating entry span', {
@@ -57,23 +73,35 @@ export default class SpanContext implements Context {
       });
     }
 
-    ContextManager.spansDup();
+    let span = this.ignoreCheck(operation, SpanType.ENTRY);
 
-    const span = new EntrySpan({
-      id: this.spanId++,
-      parentId: this.parentId,
-      context: this,
-      operation,
-    });
+    if (span)
+      return span;
+
+    const spans = ContextManager.spansDup();
+    const parent = spans[spans.length - 1];
+
+    if (parent && parent.type === SpanType.ENTRY) {
+      span = parent;
+      parent.operation = operation;
+
+    } else {
+      span = new EntrySpan({
+        id: this.spanId++,
+        parentId: this.parentId,
+        context: this,
+        operation,
+      });
 
-    if (carrier && carrier.isValid()) {
-      span.inject(carrier);
+      if (carrier && carrier.isValid()) {
+        span.extract(carrier);
+      }
     }
 
     return span;
   }
 
-  newExitSpan(operation: string, peer: string): Span {
+  newExitSpan(operation: string, peer: string, carrier?: ContextCarrier): Span {
     if (logger.isDebugEnabled()) {
       logger.debug('Creating exit span', {
         parentId: this.parentId,
@@ -81,15 +109,32 @@ export default class SpanContext implements Context {
       });
     }
 
-    ContextManager.spansDup();
+    let span = this.ignoreCheck(operation, SpanType.EXIT);
 
-    return new ExitSpan({
-      id: this.spanId++,
-      parentId: this.parentId,
-      context: this,
-      peer,
-      operation,
-    });
+    if (span)
+      return span;
+
+    const spans = ContextManager.spansDup();
+    const parent = spans[spans.length - 1];
+
+    if (parent && parent.type === SpanType.EXIT) {
+      span = parent;
+
+    } else {
+      span = new ExitSpan({
+        id: this.spanId++,
+        parentId: this.parentId,
+        context: this,
+        peer,
+        operation,
+      });
+
+      // if (carrier && carrier.isValid()) {  // is this right?
+      //   Object.assign(carrier, span.inject());
+      // }
+    }
+
+    return span;
   }
 
   newLocalSpan(operation: string): Span {
@@ -100,6 +145,11 @@ export default class SpanContext implements Context {
       });
     }
 
+    let span = this.ignoreCheck(operation, SpanType.LOCAL);
+
+    if (span)
+      return span;
+
     ContextManager.spansDup();
 
     return new LocalSpan({
@@ -124,11 +174,11 @@ export default class SpanContext implements Context {
   stop(span: Span): boolean {
     logger.debug('Stopping span', { span: span.operation, spans: ContextManager.spans, nSpans: this.nSpans });
 
-    if (span.finish(this.segment)) {
-      const idx = ContextManager.spans.indexOf(span);
-      if (idx !== -1) {
-        ContextManager.spans.splice(idx, 1);
-      }
+    span.finish(this.segment);
+
+    const idx = ContextManager.spans.indexOf(span);
+    if (idx !== -1) {
+      ContextManager.spans.splice(idx, 1);
     }
 
     if (--this.nSpans == 0) {
diff --git a/src/trace/span/DummySpan.ts b/src/trace/span/DummySpan.ts
index f5387c8..8657bfb 100644
--- a/src/trace/span/DummySpan.ts
+++ b/src/trace/span/DummySpan.ts
@@ -18,5 +18,14 @@
  */
 
 import Span from '../../trace/span/Span';
+import { ContextCarrier } from '../context/ContextCarrier';
 
-export default class DummySpan extends Span {}
+export default class DummySpan extends Span {
+  inject(): ContextCarrier {
+    return new ContextCarrier();
+  }
+
+  extract(carrier: ContextCarrier): this {
+    return this;
+  }
+}
diff --git a/src/trace/span/EntrySpan.ts b/src/trace/span/EntrySpan.ts
index 4b8126e..59e841b 100644
--- a/src/trace/span/EntrySpan.ts
+++ b/src/trace/span/EntrySpan.ts
@@ -42,8 +42,8 @@ export default class EntrySpan extends StackedSpan {
     return this;
   }
 
-  inject(carrier: ContextCarrier): this {
-    super.inject(carrier);
+  extract(carrier: ContextCarrier): this {
+    super.extract(carrier);
 
     const ref = SegmentRef.fromCarrier(carrier);
 
diff --git a/src/trace/span/ExitSpan.ts b/src/trace/span/ExitSpan.ts
index 8c16db6..745e4c4 100644
--- a/src/trace/span/ExitSpan.ts
+++ b/src/trace/span/ExitSpan.ts
@@ -33,7 +33,7 @@ export default class ExitSpan extends StackedSpan {
     );
   }
 
-  extract(): ContextCarrier {
+  inject(): ContextCarrier {
     return new ContextCarrier(
       this.context.segment.relatedTraces[0],
       this.context.segment.segmentId,
diff --git a/src/trace/span/Span.ts b/src/trace/span/Span.ts
index d854e9a..3e245a2 100644
--- a/src/trace/span/Span.ts
+++ b/src/trace/span/Span.ts
@@ -80,9 +80,6 @@ export default abstract class Span {
 
   stop(): this {
     logger.debug(`Stopping span ${this.operation}`, this);
-    if (this.operation === '/test') {
-      console.info('kkkkkkkkkkkkkkkkkkkkkkkkkl')
-    }
     this.context.stop(this);
     return this;
   }
@@ -107,14 +104,14 @@ export default abstract class Span {
   }
 
   // noinspection JSUnusedLocalSymbols
-  extract(): ContextCarrier {
+  inject(): ContextCarrier {
     throw new Error(`
-      can only extract context carrier into ExitSpan, this may be a potential bug in the agent,
+      can only inject context carrier into ExitSpan, this may be a potential bug in the agent,
       please report this in ${packageInfo.bugs.url} if you encounter this.
     `);
   }
 
-  inject(carrier: ContextCarrier): this {
+  extract(carrier: ContextCarrier): this {
     this.context.segment.relate(carrier.traceId!);
 
     return this;
diff --git a/src/trace/span/StackedSpan.ts b/src/trace/span/StackedSpan.ts
index a06f1c2..8c1886f 100644
--- a/src/trace/span/StackedSpan.ts
+++ b/src/trace/span/StackedSpan.ts
@@ -38,7 +38,10 @@ export default class StackedSpan extends Span {
     return this;
   }
 
-  finish(segment: Segment): boolean {
-    return --this.depth === 0 && super.finish(segment);
+  stop(): this {
+    if (--this.depth === 0) {
+      super.stop();
+    }
+    return this;
   }
 }
diff --git a/tests/plugins/http/expected.data.yaml b/tests/plugins/http/expected.data.yaml
index 288515b..29e6cff 100644
--- a/tests/plugins/http/expected.data.yaml
+++ b/tests/plugins/http/expected.data.yaml
@@ -31,7 +31,7 @@ segmentItems:
             componentId: 49
             isError: false
             spanType: Entry
-            peer: ''
+            peer: server:5000
             skipAnalysis: false
             tags:
               - { key: http.url, value: server:5000/test }
@@ -71,7 +71,7 @@ segmentItems:
             componentId: 49
             isError: false
             spanType: Entry
-            peer: ''
+            peer: localhost:5001
             skipAnalysis: false
             tags:
               - { key: http.url, value: localhost:5001/test }